【Java】多线程 - Java中的线程状态及状态切换

Posted by 西维蜀黍 on 2019-01-31, Last Modified on 2021-09-21

Java 的线程状态

Java 语言中定义了 6 种线程状态,在任意一个时间点,一个线程有且只有其中一种状态,这 6 种状态是:

  • 新建(New):一个已经被实例化线程对象但未被启动(start)的线程处于这种状态。
  • 可运行(Runnable):包括了操作系统线程状态中的 Running 和 Ready。因此,处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间(CPU时间片)
  • 无限期等待(Waiting):线程处于无限期等待表示它需要等待并获得其他线程的指示后才能继续运行。处于这种状态的线程不会被分配 CPU 执行时间,直到当其他的一个线程完成了一个操作后显式地通知这个线程。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配 CPU 执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由操作系统自动唤醒,以结束限期等待状态。
  • 阻塞(Blocked):线程被阻塞了,在等待进入同步区域(synchronized)时,线程将进入这种状态。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
  • 结束(Terminated):已终止的线程状态,线程已经结束执行。

注意,以上所指的线程状态均是 Java 线程在 JVM 中的状态,因此并不意味着此时对应于操作系统中的线程状态。

关于锁的持有

  • Object.wait() 方法会释放 CPU 执行权和占有的锁。
  • Thread.sleep(long) 方法仅释放 CPU 使用权,锁仍然占用。
  • Thread.yield() 方法仅释放 CPU 执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。

等待(waiting)与阻塞(blocked)的区别

blocked 是过去分词,意味着他是被卡住的。因为这段代码在同一时刻只让一个线程执行。同时,JVM 会负责多个线程进入同步区的调度工作。因此,只要别的线程退出这段代码(放弃持有互斥锁),JVM 就会自动让你进去。也就是说别的线程无需唤醒你,由 JVM 自动来干

而 waiiting 是说线程自己调用 wait() 等函数,主动卡住自己,请 JVM 在满足某种条件后(比如另外的线程调用了notify()),把我唤醒。这个唤醒的责任在于别的线程,唤醒的方法为显式地调用一些唤醒函数(比如,o.notifyAll()或o.notify())。

一句话概括,WAITTING 线程是自己现在不想要 CPU 时间片(因而暂停自己),但是 BLOCKED 线程是想要的,只是 BLOCKED 线程没有获得锁,所以它获取不到 CPU 时间片

做这样的区分,是 JVM 出于管理的需要。如果别的线程运行出了synchronized 这段代码,JVM 只需要去 blocked 队列,放一个线程出来。而某个线程调用了 notify() 后,JVM 只需要去 waitting 队列里取一个线程出来。

P.S. 从Linux内核来看,这些线程都是阻塞状态(Blocked),没区别,区别只在于 Java 的管理需要。在操作系统中,有三种状态:

  • 运行(Running):在这一刻使用CPU。
  • 就绪(Ready):可以被运行(runnable),只是暂时地被停止以让其他进程运行。
  • 阻塞(Blocked):不能被运行直到一些依赖的外部事件发生。

通常我们在系统级别说线程的阻塞(Blocked),是说在线程进行I/O操作时,因为I/O数据没有就绪因而需要等待,这种线程由 Linux 内核来唤醒(当I/O数据到来时,内核就把 Blocked 的线程放进可运行的线程队列,依次得到CPU时间)。而在系统级别说线程的wait,是指线程等待一个内核mutex对象,另个线程signal这个mutex后,这个线程才可以运行。

这里的区别在于由谁唤醒,是操作系统,还是另一个线程。

线程的生命周期(Life Cycle)

线程间的状态转换

1 新建(New) - 就绪(Ready)- 运行中(Running)

新创建了一个线程对象后,其他线程(如main线程)调用该线程对象的 start() 方法以使其进入就绪状态。

处于就绪状态的线程位于可运行线程池中,等待被线程调度选中后可获取CPU 的使用权。

运行状态(Running)的线程会获得了CPU 时间片(timeslice) ,并执行程序代码。

Thread thread = new Thread();
thread.start();

不区分就绪(Ready)和运行中(Running)

我们可能会问,为何 JVM 中没有去区分就绪(Ready)和运行中(Running)这两种状态,而是统一归为**可运行(Runnable)**呢?

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的**时间分片(time quantum or time slice)**方式进行抢占式(preemptive)轮转调度(round-robin)的。

这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如10-20ms 的时间(此时处于 Running 状态),也即大概只有0.01秒这一量级,时间片用完后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 Ready 状态)

通常,Java的线程状态是服务于监控的,如果线程切换得是如此之快,那么区分 Ready 与 Running 就没什么太大意义了。

start() 和 run() 方法的区别

start()

Thread类的 start 方法被用来启动一个线程,并将其置为就绪状态。run 方法为这个线程真正要执行的方法。然而,调用 start 方法后,run 方法并不会立刻被调用,而是等待合适的时机时,线程调度器才会将这个线程置为运行(running)状态,最终 run 方法被调用。

这意味着,当调用 start 方法时,无需等待 run 方法体代码执行而直接继续执行下面的代码。一旦得到 CPU 时间片,就开始执行 run 方法,run方法运行结束,此线程随即终止。

run()

run() 方法只是类的一个普通方法而已,如果直接调用 run 方法,程序中依然只有主线程这一个线程(而不会创建一个新线程),其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到开启新线程的目的。

总结:调用 start 方法方可启动线程,而 run 方法只是一个普通方法的,因此在主线程中调用 run 方法,不会开启一个新线程,而只会在主线程中执行这个 run 方法中的方法体。

2 运行中(Running) - 就绪(Ready)

Thread.yield() 的调用显式地使线程调度器(Thread Scheduler)暂停当前处于 Running 状态的线程,让出 CPU 时间片以让其他线程执行。

3 进入限期等待(Timed Waiting)状态

限期等待(Timed Waiting),也可以称作 TIMED_WAITING(有等待时间的等待状态)。限期等待状态是线程自己主动发起的,

线程主动调用自己的以下方法,会使其进行限期等待(Timed Waiting):

  • Thread.sleep(sleeptime),且带有时间;
  • Object.wait(timeout),且带有时间;
  • Thread.join(timeout),且带有时间;
  • LockSupport.parkNanos(),且带有时间;
  • LockSupport.parkUntil(),且带有时间;

4 进入无限期等待(Waiting)状态

处于无限期等待(Waiting)的线程限期等待状态是线程自己主动发起的,

可运行状态的线程调用以下方法,会进入无限期等待状态:

  • Object.wait()方法,并且没有使用timeout参数;
  • Thread.join()方法,没有使用timeout参数;
  • LockSupport.park();
  • Conditon.await()方法。

5 进入阻塞(Blocked)状态

阻塞状态是指线程因为某种原因JVM放弃了其 CPU 的使用权,暂时停止运行。

阻塞的情况分两种:

  • 进入synchronized方法:运行(Running)的线程进入了一个synchronized方法时,若该互斥锁(Mutual-exclusion lock)被别的线程占用,则JVM会把该线程放入锁池(lock pool)中,且该线程进入阻塞状态;若该同步锁未被别的线程占用(该线程顺利获取到了锁),则线程状态没有变化。
  • I/O操作:运行(Running)的线程发出了I/O请求时,JVM会把该线程置为阻塞状态。当I/O处理完毕时,线程重新转入可运行(Runnable)状态。

直到线程进入可运行(Runnable)状态,才有机会再次获得CPU时间片而转到运行(Running)状态。

6 限期等待(Timed Waiting)/无限期等待(Waiting) - 阻塞(Blocked)

当一个线程调用一个对象的Object.wait()方法后,就会进入限期等待(Timed Waiting)/无限期等待(Waiting)状态。直到另一个线程调用了这个对象的 Object.notify() or Object.notifyAll() ,且这个线程希望持有的锁被其他线程持有,就会从等待状态切换到阻塞状态。

7 限期等待(Timed Waiting) - 可运行(Runnable)

在到达指定时间后,处于限期等待的线程会恢复可运行状态。

8 进入结束(Terminated)状态

线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

Reference