前言
线程的“生老病死”,我们称之为生命周期。
那java线程的生命周期都有哪些个阶段呢?请看下图(图片来源于网络):
重点是各个状态直接的转化在java代码层面上如何体现,你需要好好理解。
new(初始化状态)
新建一个线程对象。
例如下面代码,new了一个MyThread的对象t1,那么t1线程就是初始化状态。
// 自定义一个线程类
class MyThread extends Thread {
public void run() {
// 省略业务代码
......
}
}
// 创建线程对象
MyThread t1= new MyThread();
runnable(可运行(就绪)/ 运行状态)
在java线程层面上,runnable状态包括可运行(也叫就绪)状态和运行状态。
- 可运行转态(runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获得CPU并执行程序代码。
在代码上体现:
// 从new状态转换到runnable状态
myThread.start();
blocked(阻塞状态)
runnable状态到blocked状态,只有一种场景:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中,线程进入阻塞状态。阻塞的线程放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
waiting(无时限等待)
有三种场景会导致线程从runnable到waiting
- 第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法,调用该方法的线程进入waiting状态。当其他的线程获得对应对象锁之后,调用notify或notifyall方法,waiting的线程才转换到runnable状态。
notify和notifyAll的区别:
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。
sleep和wait的区别:
-
当线程执行sleep方法时,不会释放当前的锁(如果当前线程进入了同步锁),也不会让出CPU。sleep(milliseconds)可以用指定时间使它自动唤醒过来,如果时间不到只能调用interrupt方法强行打断。
-
当线程执行wait方法时,会释放当前的锁,然后让出CPU,进入等待状态。只有当notify/notifyAll被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait() ,再次释放锁。
-
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”
-
notify/notifyAll的执行只是唤醒沉睡的线程,而不会立即释放锁,必须执行完notify()方法所在的synchronized代码块后才释放。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区。
-
第二种场景,调用无参数的 Thread.join() 方法。其中的 join() 是一种线程同步方法,例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 runnable转换到 waiting。当线程 thread A 执行完,原来等待它的线程又会从 waiting状态转换到 runnable。
-
第三种场景,调用 LockSupport.park() 方法。Java 并发包中的锁,都是基于LockSupport实现的。调用 > > LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 runnable转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 waiting状态转换到 runnable。
park()与unpark():
concurrent包是基于AQS(AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:
- Unsafe(提供CAS操作)
- LockSupport(提供park/unpark操作)
LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码。
park与unpark的特点
park/unpark的设计原理核心是“许可”(permit):park是等待一个许可,unpark是为某线程提供一个许可。permit不能叠加,也就是说permit的个数要么是0,要么是1。也就是不管连续调用多少次unpark,permit也是1个。线程调用一次park就会消耗掉permit,再一次调用park又会阻塞住。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
unpark可以先于park调用。在使用park和unpark的时候可以不用担心park的时序问题造成死锁。相比之下,wait/notify存在时序问题,wait必须在notify调用之前调用,否则虽然另一个线程调用了notify,但是由于在wait之前调用了,wait感知不到,就造成wait永远在阻塞。
park和unpark调用的时候不需要获取同步锁。
park与unpark的优点
与Object类的wait/notify机制相比,park/unpark有两个优点: - 以thread为操作对象更符合阻塞线程的直观定义。
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。
底层实现原理
在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。
mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。
timed_waiting(有时限等待)
有五种场景会触发这种转换:
- 调用带超时参数的 Thread.sleep(long millis) 方法;
- 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;
- 调用带超时参数的 Thread.join(long millis) 方法;
- 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
- 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
terminated(终止状态)
线程执行完 run() 方法后或者执行run() 方法的时候异常抛出,会转换到 terminated状态。
有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个stop()方法,不过已经标记为@Deprecated,所以不建议使用了。正确的姿势其实是调用**interrupt()**方法。
多线程连载(系列文章按顺序阅读哦):
Java内存模型-volatile的应用(实例讲解)
synchronized的三种应用方式(实例讲解)
可重入锁-synchronized是可重入锁吗?
大彻大悟synchronized原理,锁的升级
一文弄懂Java的线程池
公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的