Java并发9:Java线程生命周期
通用线程生命周期
通用线程生命周期基本上可以用“五态模型”来描述。
这五态是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
初始状态
线程已经被创建,但还不允许分配 CPU 执行。
这个状态属于编程语言特有,这里所谓的被创建,仅是在编程语言层面被创建,而在操作系统层面,线程还没有创建。
可运行状态
线程可以分配 CPU 执行。
这个状态,真操作系统线程已经被成功创建,可以分配 CPU 执行。
运行状态
当有空闲的 CPU ,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成运行状态。
休眠状态
运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权。
休眠状态的线程永远没有机会获得 CPU 使用权。
当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
终止状态
线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
Java 语言里把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用。
Java语言把休眠状态给细分了。
JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。
Java线程生命周期
Java线程有6中状态:
NEW(初始化状态)
RUNNABLE(可运行/运行状态)
BLOCKED(阻塞状态)
WAITING(无限时等待状态)
TIMED_WAITING(有限时等待状态)
TERMINATED(终止状态)
线程状态转换
RUNNABLE 与 BLOCKED 的状态转换
这种情况下,等待的线程会从**RUNNABLE**转换到**BLOCKED**状态。
当等待的线程获得``` synchronized ```隐式锁时,会从**BLOCKED**转换到**RUNNABLE**状态。
线程调用阻塞式 API 时,在操作系统层面,线程会转换到休眠状态;
在 JVM 层面,Java 线程的状态不会发生变化,即Java 线程的状态依然保持**RUNNABLE**状态。
JVM 层面并不关心操作系统调度相关的状态,在 JVM 看来,等待 CPU 使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了**RUNNABLE**状态。
**Java 在调用阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。**
#### RUNNABLE 与 WAITING 的状态转换
1、
线程被调用```wait()```方法,除非被```notify()```/```notifyAll()```唤醒,否则永远没有争抢CPU执行权的资格。
该状态下的线程,已经释放了其持有的锁资源,待再次被唤醒时,需要重新争夺锁资源。
被唤醒后,重新回到**RUNNABLE**状态。
2、
线程被调用```Thread.join()```方法,执行该方法的线程会由**RUNNABLE**转换为**WAITING**状态,等到目标线程执行完毕,执行该方法的线程会回到**RUNNABLE**状态。
3、
调用```LockSupport.park()```方法,当前线程会由**RUNNABLE**转换为**WAITING**状态。
调用``` LockSupport.unpark(Thread thread)```可唤醒目标线程,目标线程的状态又会从**WAITING**状态转换到**RUNNABLE**。
**通过 `Lock.lock()` 方法等待获取锁时,也会处于 WAITING 状态。**
**因为 `Lock` 接口的实现基于 AQS 实现的,而 AQS 中的阻塞操作都是基于 `LockSupport` 工具类实现的。**
#### RUNNABLE 与 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)``` 方法。
+ 调用带超时参数的```Lock.tryLock(long time, TimeUnit unit)```方法。
**TIMED_WAITING**和**WAITING**状态的区别,仅仅是触发条件多了超时参数。
#### 从 NEW 到 RUNNABLE 状态
继承```Thread```对象或实现```Runnable```接口,重写```run()```方法来创建线程。
调用start()方法将线程从**NEW**转换到**RUNNABLE**状态。
#### 从 RUNNABLE 到 TERMINATED 状态
+ 执行完```run()```方法
+ ~~```stop()```/```suspend()```/```resume()```~~方法
+ ```interrupt()```方法
```interrupt()```方法仅通知线程有机会执行一些后续操作,同时也可以无视这个通知。
被```interrupt()```的线程如何受到通知?
+ 异常
当线程 A 处于**WAITING**、**TIMED_WAITING** 状态,其他线程调用线程 A 的 ```interrupt()``` 方法,会使线程 A 返回到**RUNNABLE**状态,同时线程 A 的代码会触发 ```InterruptedException```异常。
在 `wait()` 方法中等待的线程被中断时,和使用 `notify()` 唤醒一样,必须要重新获得对象的锁才能从方法中返回,而不是立即就能返回并进入异常处理。
```wait()```、```join()```、```sleep()``` 类似的方法签名都有```throws InterruptedException```这个异常。
这个异常的触发条件就是:其他线程调用了该线程的 ```interrupt()```方法。
note:
当线程 A 处于**RUNNABLE**状态时,并且阻塞在 ```java.nio.channels.InterruptibleChannel``` 上时,如果其他线程调用线程 A ``` interrupt()``` 方法,线程 A 会触发 ```java.nio.channels.ClosedByInterruptException``` 这个异常;而阻塞在```java.nio.channels.Selector```上时,如果其他线程调用线程 A 的 ```interrupt()``` 方法,线程 A 的 ```java.nio.channels.Selector``` 会立即返回。
+ 主动检测
如果线程处于**RUNNABLE**状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时需要依赖线程 A 主动检测中断状态。
其他线程调用线程 A 的```interrupt()```方法,线程 A 可通过```isInterrupted()```方法检测自己是否被中断。
### 思考
```java
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
上面代码的本意是当前线程被中断之后,退出while(true)
,这段代码是否正确?
可能出现无限循环,
如果在线程在sleep期间被中断,会抛出一个InterruptedException
异常,在触发InterruptedException
异常的同时,JVM 会同时把线程的中断标志位清除。
应该重置一下中断标示。
InterruptedException - if any thread has interrupted the current thread.
The interrupted status of the current thread is cleared when this exception is thrown.
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
}catch (InterruptedException e){
//重置中断标识
th.interrupt();
e.printStackTrace();
}
}
别人的总结
在 Java 的文档中对
interrupt()
的效果列了四种情形:If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If this thread is blocked in an I/O operation upon an InterruptibleChannel then the channel will be closed, the thread’s interrupt status will be set, and the thread will receive a ClosedByInterruptException.
If this thread is blocked in a Selector then the thread’s interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector’s wakeup method were invoked.
If none of the previous conditions hold then this thread’s interrupt status will be set.
前三种情形其实是描述了如果线程处于等待状态或是阻塞在某一种资源上,那么
interrupt()
方法会使得线程跳出这种状态继续执行下去。第四种情形则描述了如果线程正在正常执行,那么interrupt()
的效果则是设置了线程的中断状态,至于怎么处理这种状态,可以选择忽略也可以按需处理。
yield()
方法是Thread
类的静态方法,也用于出让当前线程占用的CPU资源。和sleep(long)
方法不同的是,sleep(long)
会使得线程进入WAITING
状态并且至少会等待超时时间到达后才会再次执行;而yield()
方法则是从RUNNING
进入READY
状态(这里指的是操作系统层面,在 JVM 暴露出来的都是RUNNABLE
状态),因而极有可能马上又被调度选中继续运行。A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.
从文档中的表述来看,
yield()
方法相比于sleep(long)
方法更依赖与系统的调度。该方法并不经常用到。——jrwang