java多线程超详细总结
1、线程概述
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
2、线程与进程
进程概述:
几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程( Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程特征:
1、独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
2、动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的
3、并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
线程:
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
并发和并行:
并发:同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行
并行:同一时刻,有多条指令在多个处理器上同时执行
多线程:
概述:
多线程就是几乎同时执行多个线程(一个处理器在某一个时间点上永远都只能是一个线程!即使这个处理器是多核的,除非有多个处理器才能实现多个线程同时运行。)。几乎同时是因为实际上多线程程序中的多个线程实际上是一个线程执行一会然后其他的线程再执行,并不是很多书籍所谓的同时执行。
多线程优点:
1、进程之间不能共享内存,但线程之间共享内存非常容易。
2、系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
3、Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程
3、使用多线程:
多线程的创建:
(1)、继承Thread类:
第一步:定义Thread类的之类,并重写run方法,该run方法的方法体就代表了线程需要执行的任务
第二步:创建Thread类的实例
第三步:调用线程的start()方法来启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FirstThread extends Thread {
private int i;
public void run() {
for(;i<100;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
new FirstThread().start();
new FirstThread().start();
}
}
}
}
(2)、实现Runnable接口:
第一步:定义Runnable接口的实现类,并重写该接口的run方法,该run方法同样是线程需要执行的任务
第二步:创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SecondThread implements Runnable {
private int i;
@Override
public void run() {
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
SecondThread s1=new SecondThread();
new Thread(s1,"新线程1").start();;
new Thread(s1,"新线程2").start();
}
}
}
}
(3)、使用Callable和Future创建线程
细心的读者会发现,上面创建线程的两种方法。继承Thread和实现Runnable接口中的run都是没有返回值的。于是从Java5开始,Java提供了Callable接口,该接口是Runnable接口的增强版。Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
创建并启动有返回值的线程的步骤如下:
第一步:创建 Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建 Callable实现类的实例。从Java8开始,可以直接使用 Lambda表达式创建 Callable对象
第二步:使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值
第三步:使用FutureTask对象作为Thread对象的target创建并启动新线程
第四步:通过FutureTask的get()方法获得子线程执行结束后的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThirdThread {
public static void main(String[] args) {
//ThirdThread rt=new ThirdThread();
FutureTask task=new FutureTask((Callable)()->{
int i=0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+"的循环变量i"+i);
}
return i;
}) ;
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+"的循环变量i为"+i);
if(i==20) {
new Thread(task,"有返回值的线程").start();;
}
}
try {
System.out.println("子线程的返回值"+task.get());
}catch(Exception e) {
e.printStackTrace();
}
}
}
创建线程的三种方式的对比:
采用Runnable、Callable接口的方式创建多线程的优缺点:
优点:
1、线程类只是实现了 Runnable接口或 Callable接口,还可以继承其他类
2、在这种方式下,多个线程可以共享同一个 target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
缺点:
编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承 Thread类的方式创建多线程的优缺点:
优点:
编写简单,如果需要访问当前线程,则无须使用 Thread.current Thread()方法,直接使用this即可获得当前线程
缺点:
因为线程已经继承了Thread类,所以不能再继承其他类
线程的生命周期:
新建和就绪状态:
当程序使用new关键字创建一个线程后,该线程就处于新建状态。
当线程对象调用了start()方法后,该线程就处于就绪状态。
运行和阻塞状态:
如果处于就绪状态的线程获取了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
当线程调用sleep(),调用一个阻塞式IO方法,线程会被阻塞
死亡状态:
1、run()或者call()方法执行完成,线程正常结束
2、线程抛出一个未捕获的Exception或Error
3、直接调用该线程的stop方法来结束该线程——该方法容易导致死锁,不推荐使用
线程状态转化图
4、控制线程:
(1)、join线程
Thread提供了让一个线程等待另一个线程完成的方法——join方法。当在某个程序执行流中调用其直到被 join方法加入的join线程执行完为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JoinThread extends Thread {
//提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name) {
super(name);
}
//重写run方法,定义线程体
public void run() {
for(int i=0;i<10;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动子线程
new JoinThread("新线程").start();
for(int i=0;i<10;i++) {
if(i==5) {
JoinThread jt=new JoinThread("被join的线程");
jt.start();
//main线程调用了jt线程的join方法,main线程
//必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
main 0
main 1
main 2
main 3
main 4
新线程 0
新线程 1
新线程 2
新线程 3
被join的线程 0
新线程 4
被join的线程 1
新线程 5
被join的线程 2
新线程 6
被join的线程 3
新线程 7
被join的线程 4
新线程 8
被join的线程 5
新线程 9
被join的线程 6
被join的线程 7
被join的线程 8
被join的线程 9
main 5
main 6
main 7
main 8
main 9
(2)、后台线程:
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程( Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用 Thread对象的 setDaemon(true)方法可将指定线程设置成后台线程。下面程序将执行线程设置成后台线程,可以看到当所有的前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DaemonThread extends Thread {
//定义后台线程的线程体与普通线程没有什么区别
public void run() {
for(int i=0;i<1000;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
DaemonThread t=new DaemonThread();
//将此线程设置为后台线程
t.setDaemon(true);
t.start();
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
//程序到此执行结束,前台线程(main)结束,后台线程也随之结束
}
}
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
main 0
Thread-0 0
main 1
Thread-0 1
Thread-0 2
main 2
Thread-0 3
Thread-0 4
Thread-0 5
main 3
main 4
Thread-0 6
main 5
Thread-0 7
Thread-0 8
main 6
main 7
main 8
Thread-0 9
main 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
(3)、线程睡眠:
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用 Thread类的静态 sleep方法来实现。 sleep方法有两种重载形式
static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态
static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加上nanos毫微秒,并进入阻塞状态,通常我们不会精确到毫微秒,所以该方法不常用
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Date;
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++) {
System.out.println("当前时间"+new Date());
Thread.sleep(1000);
}
}
}
(4)、改变线程优先级:
每个线程执行时都有一定的优先级,优先级高的线程获得较多的执行机会,优先级低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread类提供了 setPriority(int newPriority)、 getPriority()方法来设置和返回指定线程的优先级,其中 setPriority()方法的参数可以是一个整数,范围是1-10之间,也可以使用 Thread类的如下三个静态常量
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值时1
NORM_PRIPRITY:其值是5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class PriorityTest extends Thread {
//定义一个构造器,用于创建线程时传入线程的名称
public PriorityTest(String name) {
super(name);
}
public void run() {
for(int i=0;i<50;i++) {
System.out.println(getName()+",其优先级是:"+getPriority()+"循环变量的值:"+i);
}
}
public static void main(String[] args) {
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for(int i=0;i<30;i++) {
if(i==10) {
PriorityTest low=new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:"+low.getPriority());
//设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20) {
PriorityTest high=new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级"+high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
5、线程同步:
(1)、线程安全问题:
现有如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Account {
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this==obj) {
return true;
}
if(obj!=null&&obj.getClass()==Account.class) {
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.alibaba.util.Account;
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望的钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//多个线程修改同一个共享数据,可能发生线程安全问题
@Override
public void run() {
if(account.getBalance()>drawAmount) {
System.out.println(getName()+"取钱成功"+" "+drawAmount);
try {
Thread.sleep(1);
}catch(Exception e) {
e.printStackTrace();
}
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为"+" "+account.getBalance());
}else {
System.out.println("余额不足,取钱失败");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.util.Account;
public class DrawTest {
public static void main(String[] args) {
Account account=new Account("1234567",1000);
//模拟两个线程同时操作账号
new DrawThread("甲", account, 800).start();;
new DrawThread("乙", account, 800).start();;
}
}
现在我们来分析一下以上代码:
我们现在希望实现的操作是模拟多个用户同时从银行账户里面取钱,如果用户取钱数小于等于当前账户余额,则提示取款成功,并将余额减去取款钱数,如果余额不足,则提示余额不足,取款失败。
Account 类:银行账户类,里面有一些账户的基本信息,以及操作账户信息的方法
DrawThread类:继承了Thread,是一个多线程类,用于模拟多个用户操作同一个账户的信息
DrawTest:测试类
这时我们运行程序可能会看到如下运行结果:
1
2
3
4
甲取钱成功 800.0
乙取钱成功 800.0
余额为 200.0
余额为 -600.0
余额竟然为-600,余额不足也能取出钱来,这就是线程安全问题。因为线程调度的不确定性,出现了偶然的错误。
(2)、如何解决线程安全问题:
①、同步代码块:
为了解决线程问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj){
//此处的代码就是同步代码块
}
我们将上面银行中DrawThread类作如下修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import com.alibaba.util.Account;
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望的钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//多个线程修改同一个共享数据,可能发生线程安全问题
@Override
public void run() {
//使用account作为同步监视器,任何线程在进入下面同步代码块之前
//必须先获得account账户的锁定,其他线程无法获得锁,也就无法修改它
//这种做法符合:"加锁-修改-释放锁"的逻辑
synchronized(account) {
if(account.getBalance()>dra