![b56ecff9a67d598d20ebef8a255cb54f.png](https://i-blog.csdnimg.cn/blog_migrate/cee743296268e294666625b6e3aa187a.png)
摘要:本文主要介绍线程、线程的基本操作(工作状态、新建、终止、中断、休眠、挂起、唤醒、谦让、并入)、守护线程、线程的优先级以及基本的线程同步与通信操作(Synchronized、wait、notify)
1. 什么是线程?
官方解释:线程是进程的最小执行单位。
打开任务管理器,可以发现计算机上运行着很多进程,如下:
![9e4e370f6b236f7a0d54e73e77f60ff9.png](https://i-blog.csdnimg.cn/blog_migrate/a9dea9d1b02e2a257e17e1b967dc9037.jpeg)
细心观察一下,可以知道,进程就是当前运行在计算机上的应用程序。而一个进程包含着若干个线程,因此而线程是进程的最小执行单位,线程共享进程的资源,而进程间是相互独立的。
总结:在计算机上运行的每一个应用程序称为一个进程,线程是进程的最小执行单位,也是CPU调度的最小单位,而进程是CPU进行资源分配的最小单位,线程共享进程的资源,进程和进程间相互独立存在。
--问题:问什么使用CPU调度的最小单位是线程,而不是进程?
因为进程的上下文切换是非常重量级的,而线程相对而言是轻量级的,但是带来的一个问题就是,程序会变得相对复杂一些。
注意事项:在java中创建一个线程,等同在操作系统上创建了一个线程,因为JVM会自动将java线程映射到操作系统线程。
2. 线程的基本操作
--线程的工作状态
在java中创建一个线程之后,线程会有如下一些工作状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
--状态转换图:
![decea0f5797954d60d08640102b3c7a7.png](https://i-blog.csdnimg.cn/blog_migrate/05fdab533989ee90c1cff12c99bf773b.png)
--NEW:通过new关键字创建一个线程的时候,线程进入NEW状态(新生状态)。
--RUNNABLE:当执行线程的start方法的时候,线程就进入RUNNABLE状态(可运行状态),需要注意的是,RUNNABLE状态,仅仅只是表示线程需要的资源一切准备就绪,但不一定在运行,因为线程运行需要CPU资源,而CPU资源需要操作系统进行分配,因此线程即使满足了所有非硬件资源,但是CPU资源没有到达,仍然无法运行。
--TERMINATED:当线程完成了所有的工作或者被其他线程中断之后,就会进入TERMINATED状态(死亡状态)。
--BLOCKED:当线程申请某个已被申请的共享资源时,就会进入BLOCKED状态(阻塞状态)。当共享资源被释放之后,处于BLOCKED的线程就会重新进入RUNNABLE状态,因为其所需的资源已到达。
--WAITING:当线程因某个同步监视器进入等待,并且是无限期时,就会进入WAITING状态(等待状态)。
--TIMED_WAITING:当线程因某个同步监视器进入等待,并且是有期限时,就会进入TIMED_WAITING状态(有期限的等待)。
注意:BLOCKED、WAINTING、TIMED_WAITING都会使得线程挂起停止工作,但是BLOCKED是因为等待某个同步监视器锁而挂起,而WAITING、TIMED_WAITING分为是因为线程调用了wait()、wait(long time)而挂起,当同步监视器锁被释放之后,处于BLOCKED的线程就会自动进入RUNNABLE状态,而处于WAITING的线程需要其他线程通过notify方法唤醒,进入RUNNABLE状态,处于TIMED_WITING的线程当等待时间到了之后,就会进入RUNNALBE状态。
--新建线程
第一种方式:Thread类就代表线程,因此只需继承Thread类,重写run方法,即可新建一个线程,如下:
public
启动一个线程则是调用Thread类上的start方法,如下:
public
当通过new的方式创建MyThread之后,MyThread线程就进入NEW状态,当执行start方法之后,线程就会进入RUNNALBE状态,此时等待CPU调度,执行重写的run方法。
第二种方式:实现Runnable接口,重写run方法。如下:
public
启动线程方式和第一种稍稍不同,需要将Runnable实现类作为Thread类的构造方法参数创建线程。如下:
public
这两种方式,都和Thread直接关系,因此提出假设:创建线程只有一种方式,那就是继承Thread类。
--论证:
第二种方式调用了Thread类的有参构造方法,如下:
public
方法调用链:
private
沿着方法调用链可以发现,传入的Runnable接口作为Thread类的一个成员变量存在,如下:
/* What will be run. */
此时,再来看Thread的run方法:
@Override
可以发现就是调用传入Runnable接口的run方法。
因此可以得出结论实第二种方式无非是第一种方式的变种。区别如下:
![ae542d5e7ad642dc426b6960154063bd.png](https://i-blog.csdnimg.cn/blog_migrate/3cb89efc3f9007a22c3ec8f11e0519b6.jpeg)
--常见问题:执行run方法开启线程。
run方法不是程序员调用的,而是操作系统调用了,如果手动执行run方法,意味着直接在当前执行run方法的线程中执行了run方法一次,而达不到线程调用的效果,需要特别注意!!! --终止线程
Thread类提供了Stop方法用于终止线程,并且线程释放所有持有的锁,如下:
@Deprecated
但是不推荐使用stop方法来终止线程,因为是stop方法太过暴力了,会出现数据错乱问题,如下案例:
--假设有一条记录:ID = 1, NAME = 小明。
--需求:将这条记录值设置到对象User中。
--此时有读写两个线程操作对象User,首先读线程拿到Lock,对User进行赋值,正当写线程赋值完ID之后,给NAME赋值时,程序通过stop方法终止了写线程,导致User对象的ID值为1,但是NAME值仍为NULL,此时读线程拿到Lock,并读取User对象的值,此时读取到的值都是不正确的,有误的。如下图:
![086719f4874d5d2ecaf9d5374406b786.png](https://i-blog.csdnimg.cn/blog_migrate/9b2c997e3ba5d2245e7664045506ac93.png)
--中断线程
Thread类提供了三个方法用于中断线程:
public void interrupt() // 中断线程
public boolean isInterrupted() // 判断线程是否被中断
public static boolean interrupted() // 判断线程是否被中断,并清除当前中断状态
线程中断也是用于停止一个线程,但是相对于stop方法,终止会更加优雅。
线程中断可以这样理解:如果线程在工作过程中,可以给它发送一条消息,给它打一个招呼,那么中断相当于给线程打了一个招呼,随后线程就会把中断标志位给置上。那么此时线程就会知道有其他线程可能需要我做一些响应或者一些事情,那么此时线程就会完成一些额外的工作,比如说之前讲解stop方法的案例,如果使用stop来停止线程,那么数据的一致性很难得到保证,那么此时可以尝试使用interrupt来停止线程。如上面修改User对象案例,写线程可以在修改数据之前,查看一下中断标志位是否置上,如果置上,那么就中断修改。即是在写线程修改过程中,置上了中断标志也不会导致写线程立即中断,导致数据不一致。因此,通过中断来停止线程是一个非常不错的选择。如下:
@Override
中断无效案例:
![29385dfd4294217bf44a5b6b58d06ebb.png](https://i-blog.csdnimg.cn/blog_migrate/bd221e923ec014a8db5d9d68af58599b.jpeg)
--线程休眠
Thread类提供了sleep方法用于线程休眠:
public static native void sleep(long millis) throws InterruptedException;
在线程休眠过程中,如果希望中断线程,那么也是可以的,并且中断之后会抛出一个InterruptedException,但是需要注意的是,一旦抛出InterruptedException,并被捕获处理之后,线程的中断标识位就会被清除,即为false,所以如果想要休眠的线程被中断,需要在catch语句块中,重新设置线程的中断标识位。如下:
@Override
注意事项:sleep方法仅仅只是想让线程停止工作片刻,因此休眠的线程不会释放持有的锁!!!
--线程挂起(suspend)和唤醒(resume)
Thread中提供了suspend方法用于挂起线程:
@Deprecated
Thread中提供了resume方法用于唤醒线程:
@Deprecated
suspend和resume方法都标注了@Deprecated注解,意味着这两个方法都不建议使用,仅仅只是为了向前兼容而保留下来。既然不建议使用,那么就肯定有不足所在,我们可以查看suspend方法的源码注释来看下不足:
*
注释的大致意思就是:当线程被suspend挂起之后,其所持有的的锁不会释放,直到这个线程被resume唤醒,由于不确定线程挂起和唤醒的先后顺序,因此有可能会出现先resume线程,接着才是suspend线程,那么就会导致线程持有的锁,永远都不被释放,从而造成线程冻结的现象出现。
总的来说,由于suspend挂起的线程不会释放其所持有的的锁,因此有可能出现死锁的情况。
--如下图所示:
![2a61e2d676481936a2e5644e3d720bdf.png](https://i-blog.csdnimg.cn/blog_migrate/f8cbdaed77693216aed2e43f4786a222.jpeg)
--代码如下:
// 用于展示suspend冻结类
--运行结果:
![3aec639c5954f5a5667a15d37eb91d88.png](https://i-blog.csdnimg.cn/blog_migrate/00192cb08acd394b44628c7194eba879.png)
通过运行结果,可以知道,线程1和线程2都成功启动,但是主线程等了许久都没有停止,意味着,线程1、线程2中存在没有停止工作的线程,接着使用JPS工具查看当前运行的线程有哪些:
![960fc5b63d9be08082fc9c3f7c4f3975.png](https://i-blog.csdnimg.cn/blog_migrate/082ac7f8c6b65806de73eae33379491b.png)
可以发现,主线程确实还在运行,接着使用jstack 工具查看主线程中的所有子线程:
![7d7f115e81f29f9595e551c4b4909797.png](https://i-blog.csdnimg.cn/blog_migrate/cf25539e2fd5c6ca45337f2d2e04828a.jpeg)
查看查询到的所有子线程中,只有t2线程是我们创建的,其他的都是守护线程,由于t2没有执行完毕,所以主线程也没有停止,是正常的。这里我们可以推测一下多线程的运行轨迹:
![d9dda4eb26b957ebb4a539df392f6148.png](https://i-blog.csdnimg.cn/blog_migrate/3bb3622e1e89dcf6938e7f242215a169.jpeg)
通过这个案例,可以意识到suspend方法的危险之处,在于挂起了线程,但是不释放锁,会导致其他线程一直处于BLOCKED状态。因此不推荐使用suspend,那么与其搭配使用的resume也不推荐使用。
--线程谦让
Thread类提供了静态方法yield来实现线程谦让:
public static native void yield();
线程谦让的意思就是当前线程放弃CPU时间片,使得其他线程有更多的机会继续往下执行,但是档期CPU时间片不意味着当前线程就不再竞争CPU时间片,而是我们所有线程一起再来竞争一次CPU时间片。
注意:yield方法很少使用,一般在debug或者test的时候使用。
--线程并入
Thread类提供了两个主要的方法来实现线程并入:
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
线程并入,意味着,本来有两个线程t1、t2,是分开运行的,现在t1并入到了t2中去运行,t2停止工作,直到t1线程执行完毕之后,t2才继续运行。如下:
![e991e61ae3886517e8eae415d19828c6.png](https://i-blog.csdnimg.cn/blog_migrate/68ec5b0b460da2d3cb3fa6a948c96b44.png)
有些业务场景下,线程间是配合工作的,意味着,有些线程的执行结果可能是,另一个线程的执行参数,由于线程执行顺序是不可预测的,而有些线程必须等待另一些线程执行完毕之后,才开始执行,那么此时线程并入就显得非常有用了。
--需求:在main方法中打印1000000000
--案例代码:
public
--join方法原理:
join方法可以传入一个时间,当没有传入时间时,表示线程会无限期的等待,而传入时间时,表示等待指定时间后,并入的线程还未执行完毕也不再等待了。
--join方法源码解释:
public
当join方法执行完毕,或者等待的时间已经到了,JVM会调用notify、notifyAll方法来唤醒等待在当前线程上的所有线程。
--假设有两个线程:t1、t2,其中t1线程并入到t2中去。
那么join方法的实现原理就是,将t2线程因t1挂起,当t1执行完毕之后,再调用t1的notify、notifyAll方法来唤醒t2线程。
如果有等待时长,那么就是在等待时长内如果t1还未执行完毕,就会调用t1的notify、notifyAll方法来唤醒t1线程。
notify、notifyAll方法的作用:notify、notifyAll方法搭配wait方法执行,会唤醒所有因调用wait方法挂起的线程。
3. 守护线程
Thread提供了setDaemon方法来设置一个线程为守护线程。
public final void setDaemon(boolean on)
在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程等都可以理解为守护线程。主要注意的是:当一个java应用中,只有守护线程时,JVM就会自动退出。
--案例代码:
public
注意事项:设置一个线程为守护线程,必须在启动线程之前设置,否则会报IllegalThreadStateException异常,表示线程的状态异常。
![02d7c714847a30381151c544f228e07b.png](https://i-blog.csdnimg.cn/blog_migrate/2db478ad1629c58028ac1394fc5ae179.png)
4. 线程优先级
Thread类提供了setPriority方法用于设置线程的执行优先级。
publicfinalvoidsetPriority(int newPriority)
/**
优先级别:1-10,数值越大,优先级越大。需要注意的是:优先级级别越大意味着该线程更有可能抢占到CPU时间片,并不意味,优先级别最高就一定能争夺到CPU时间片。
--案例代码:
设置两个线程,分别完成任务:将count从0累加1,累加到100000000,看谁先执行完毕。
public
--执行结果:
![3d3ae36ec8778436064a8e5f30b90880.png](https://i-blog.csdnimg.cn/blog_migrate/307e85c7281db75995650dfef1f35e28.png)
![00218921b88a4faa8b30f86e2e19554a.png](https://i-blog.csdnimg.cn/blog_migrate/3c714523722a0ae41a978bc0ca4f89f0.png)
从执行结果,可以看出,并不是高优先级就一定能抢占CPU时间片。
5. 基本的线程同步操作
java提供了synchronized关键字和wait、notify方法来实现基本的线程同步与通信。
--Synchronized
synchronized有三种用法:
[1] 指定 加锁对象:对 给定对象加锁,进入同步代码前要获得给定对象的锁。
[2] 直接用于 实例方法:相当于对 当前实例加锁,进入同步代码前要获得当前实例的锁。
[3] 直接用于 静态方法:相当于对 当前类加锁,进入同步代码前要获得当前类的锁。
--案例代码:
[1] 指定加锁对象
public
![63c3245d3da095548f1376d9d84b441b.png](https://i-blog.csdnimg.cn/blog_migrate/e6fbaa7eb13dc7ef961f5ef5ff20615a.png)
[2] 用于实例方法
public
![63c3245d3da095548f1376d9d84b441b.png](https://i-blog.csdnimg.cn/blog_migrate/e6fbaa7eb13dc7ef961f5ef5ff20615a.png)
注意事项:加锁的时候,需要注意是否对同一个对象加锁。如下案例:
public
![742b68bdcf49a79bf65402c0fa3a7f48.png](https://i-blog.csdnimg.cn/blog_migrate/ae6178ad7e8a4ca761d6f427739cffbb.png)
在这个案例中,由于两个线程加锁的对象不是同一个,因此会出现数据不一致的问题。既然对象不是唯一的,但是类元素是唯一的,因此,可以把锁加载类上,从而实现同步。如第三种情况。
[3] 用于静态方法
public
![63c3245d3da095548f1376d9d84b441b.png](https://i-blog.csdnimg.cn/blog_migrate/e6fbaa7eb13dc7ef961f5ef5ff20615a.png)
--三种方式的状态图:
![827e9702912f3e68556ad29e53c845c7.png](https://i-blog.csdnimg.cn/blog_migrate/214b21f6d6e850daaf4e32a390965147.jpeg)
注意事项:在期望原子性操作的地方加锁,并且需要注意加锁对象是唯一的。
--wait、nofity
wait、notify方法都是Object上的方法,所有对象都可使用。
wait方法的作用是:将当前线程等待在调用wait方法的对象上。
--注意事项:
[1] 在对象调用wait方法之前,需要获取对象的锁,意味着wait方法只能在synchronized关键字的同步代码里面调用。
[2] 在对象调用wait方法之后,线程会释放对象的锁,如果不释放锁,而线程又进入等待状态,那么其他线程将无法获得线程持有的锁。
[3] 进入WAITING状态的线程,只能等待其他线程通过notify方法来唤醒。
--wait方法源码注释有详细的说明:
*
notify方法的作用是:唤醒等待在当前调用notify方法上的一个对象。
--注意事项:[1] 在对象调用notify方法之前,也需要获取对象的锁。
--案例代码:
public
![a3f444095451df2925382081d0f0cac4.png](https://i-blog.csdnimg.cn/blog_migrate/b22ac032fc6095cc08a62373e8152c3a.png)
注意:当T1线程被T2线程唤醒之后,T1线程并不能直接继续往下执行,而需要重新获取object对象的锁,而由于object对象锁已经被T2持有了,因此只有等T2释放锁之后,T1线程才能持有锁继续往下执行,从而出现了T2线程打印完end之后,在经过2秒,T1线程才开始继续执行,打印end。
--线程执行时间轴:
![4faa7fbcf3f5b3cc7e28380e435334b8.png](https://i-blog.csdnimg.cn/blog_migrate/457364b87c2d7e83e0f6575c6b67e9c0.png)
notify和notifyAll的区别:notify随机唤醒一个等待在object对象上的线程,而notifyAll则唤醒所有对象。如下图:
![aade53cd6ac2e9b4d36489c482e0d3a6.png](https://i-blog.csdnimg.cn/blog_migrate/0868d1f6eb0480e17ae21d9853bfff2c.png)