Java线程的生命周期

在Java领域,实现并发程序的主要手段就是多线程,线程是操作系统里的一个概念,虽然各种不同的开发语言如Java、C#都对其进行了封装,但是万变不离操作系统。Java语言的线程本质上就是操作系统里的线程,它们是一一对应的。
在操作系统层面,线程也有生老病死,专业的说法叫做有生命周期。对于有生命周期的事物,要学好它,思路非常简单,只要能搞懂生命周期中的各个节点的状态转换机制就可以。
虽然不同的开发语言对于操作系统进行了不同的封装,但是对于线程的生命周期这部分基本上是雷同的,所以我们可以先来了解一下通用的线程生命周期模型。这部分内容也适用于很多其他编程语言,然后再详细的有针对的学一下Java中的线程生命周期

通用的线程生命周期

通用的线程生命周期基本上可以用下图这个五态模型来描述,这五态分别是初始状态、可运行状态、运行状态、休眠状态和终止状态
在这里插入图片描述
这五态模型的详细情况如下所示。
初始状态,指的是线程已经被创建,但是还不允许分配CPU执行,这个状态属于编程语言特有的。不过这里所谓的被创建仅仅是在编程语言层面被创建,而在操作系统层面,真正线程还没有创建。
可运行状态,指的是线程可以分配CPU执行,在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态
运行状态的线程如果调用一个阻塞的API,例如以阻塞方式读文件,或者等待某个事件,例如条件变量,那么线程的状态就有可能转换到休眠状态,同时释放CPU使用权。休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
线程执行完或者出现异常,就会进入终止状态,终止状态的线程不会切换到其他任何状态。进入终止状态也就意味着线程的生命周期结束了。
这五种状态在不同编程语言里会有简化合并,例如C语言的POSIX Threads规范就是把初始状态和可运行状态合并了,Java语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。
除了简化合并,这五种状态也有可能被细化,比如Java语言里就细化了休眠状态,这个下面我们会详细讲解。

Java中线程的生命周期

介绍完通用的线程生命周期模型,想必你已经对线程的生老病死有了一个大致的了解,那接下来我们就来详细看看Java语言里的线程生命周期是什么样的。
Java语言中的线程有六种状态,分别是:
NEW(初始化状态),
RUNABLE(可运行/运行状态),
BLOCK(阻塞状态),
WAITING(无时限等待状态),
TIMED_WAITING(有时限等待)
TERMINAED(终止状态)。
这看上去挺复杂的,状态类型也比较多,但其实在操作系统层面,Java线程中的BLOCK、WAITING、TIMED_WAITING是一种状态即我们前面提到的休眠状态,也就是说,只要Java线程处于这三种状态之一,那么这个线程就永远没有CPU的使用权,所以Java线程的生命周期可以简化为下图。
在这里插入图片描述
其实可以理解为线程导致休眠状态的三种原因,那具体会是哪些情景会导致线程从RUNABLE状态转换成这三种状态呢?而这三种状态又是何时转换为RUNABLE的呢?以及NEW、TERMINAED、RUNABLE状态是如何转换的呢?

RUNABLE和BOLCKED的状态转换

只有一种场景会触发这种转换,就是线程等待synchronized的隐式锁。synchronized修饰的方法,代码块儿同一时刻只允许一个线程执行,其他线程只能等待。这种情况下,等待的线程就会从RUNABLE状态转换到BOLCKED状态,而当等待的线程获得synchronized的隐式锁时,就会从BOLCKED状态转换到RUNABLE状态。
如果你熟悉操作系统线程的生命周期的话,可能会有个疑问,线程调用阻塞式API时,是否会转换到BOLCKED的状态呢?在操作系统层面,线程是会转换到休眠状态的,但是在JVM层面。Java线程的状态并不会发生变化,也就是说,Java线程的状态会依然保持RUNABLE状态。JVM层面并不关心操作系统调度相关的状态,因为在JVM看来,等待CPU使用权(操作系统层面此时处于可执行状态),与等待IO操作(系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了RUNABLE状态。而我们平时所谓的Java在调用阻塞式API时线程会阻塞,指的是操作系统线程的状态,并不是Java线程的状态。

RUNABLE和WAITING的状态转换

第一种场景,获得synchronized隐式锁的线程,通过调用无参的Object.wait()方法,其中wait()方法我们在上一篇讲管程的时候就已经深入介绍过了,这里就不再叙述。
第二种场景,调用无参的Thread.join()方法。其中的join()是一种线程同步方法。例如有一个线程对象thread A,当调用A.join()的时候,执行这条语句的线程会等待Thread A执行完,而等待中的这个线程会从RUNABLE将转换到WAITING,当线程的thread A执行完,原来等待它的线程又会从WAITING状态转换到RUNABLE。
第三种场景,调用LockSupport.park()方法。其中的LockSupport对象也许你有点陌生,其实Java并发包中的锁都是基于它实现的。调用LockSupport.park()方法,当前的线程会阻塞,线程的状态会从RUNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNABLE

RUNABLE和TIMED_WAITING的状态转换

有五种场景会触发这种转换:
调用带超时参数的Thread.sleep(Long milis)方法;
获得synchronized隐式锁的线程,调用带超时参数的Object.wait(Long timeout)方法;
调用带超时参数的Thread.join(Long milis)方法;
调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法;
调用带超时参数的LockSupport.patkUtil(long deadline)方法。
这里你会发现TIMED_WAITING和WAITING状态的区别仅仅是触发条件多了超时参数。

从NEW到RUNNABLE状态

Java刚创建出来的thread对象就是new状态,而创建thread对象主要有两种方法。
一种是继承thread方法,重写run方法,示例代码如下:

//自定义代码对象
class MyThread extends Thread {
	public void run() {
		//线程需要执行的代码
		...
	}
}
//创建线程对象
MyThread myThread = new MyThread();

另一种是实现Runable接口,重写run方法,并将该实现类作为创建Thread对象的参数。示例代码如下:

//实现Runnable接口
class Runner implements Runnable {
	@Override
	pulic void run() {
		//线程需要执行的代码
		...
	}
}
//创建线程对象
Thread thread = new Thread(new Runner);

New状态的线程不会被操作系统调度,因此不会执行,Java线程要执行就必须转换到runable状态。从new状态转换到runable状态很简单,只要调用线程对象的star()的方法就可以了。示例代码如下:

MyThread myThread = new MyThread();
//从New状态转换到RUNNABLE状态
myThread .start();

从RUNNABLE到TERMINATED状态

线程执行完run()方法后,会自动转换到TERMINATED的状态,当然如果执行run方法的时候抛出异常,也会导致线程终止,有时候我们需要强制中断run()方法的执行。例如让方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java的thread类里面倒是有一个stop()方法,不过已经被标记为@Deprecated,所以不建议使用了。正确的姿势其实是调用interrupt()方法。
那stop()和interrupt()方法的主要区别是什么呢?
stop()方法真的会杀死线程,不给线程喘息的机会。如果线程持有的ReentrantLock锁,被stop()线程并不会自动调用ReentrantLock的unlock()去释放锁。那其他线程就再也没有机会获得ReentrantLock锁,这实在是太危险了,所以该方法就不建议使用了。类似的方法还有suspend()和resume()方法,这两个方法也同样都不建议使用了,所以这里就不多介绍。
而interrupt()方法就温柔多了,Interrupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt()的线程是如何收到通知的呢?一种是异常,另一种是主动检测。
当线程处于waiting、time_waiting状态时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到runable状态,同时线程A的代码会触发InterruptedException异常。上面我们提到转换到waiting和time_waiting状态的触发条件都是调用了类似wait()、 join()、 sleep()这样的方法,我们看这些方法的签名,发现都会throws InterruptedException这个异常。这个异常触发条件就是其他线程调用了该线程的interrupt()方法。
当线程处A处于RUNNABLE状态时,并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptibleChannel这个异常而阻塞在Java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的Java.nio.channels.Selector会立即返回。
上面这两种情况属于被中断的线程,通过异常的方式获取了通知,还有一种是主动检测,如果线程处于runable状态,并且没有阻塞在某个I/O操作上,例如中断计算圆周率的线程A。这时就得依赖线程A主动检测中断状态了。如果其他线程调用线程A的interrupt()方法,那么线程A可以通过isInterrupt()方法检测是不是自己被中断了。

总结

理解Java线程的各种状态以及生命周期对于诊断多线程bug非常有帮助。多线程程序很难调试,出了bug基本上都是靠日志,靠线程dump来跟踪问题。分析线程dump的一个基本功就是分析线程状态,大部分的死锁、饥饿、活锁问题都是需要跟踪分析线程的状态,同时本文介绍的线程生命周期具有很强的通用性。对于学习其他语言的多线程编程也有很大帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值