Thread 线程,状态转换、方法使用、原理分析
作者:小傅哥
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、前言
考不常用的、考你不会的、考你忽略的,才是考试!
大部分考试考的,基本都是不怎么用的。例外的咱们不说😄 就像你做程序开发,尤其在RPC+MQ+分库分表,其实很难出现让你用一个机器实例编写多线程压榨CPU性能。很多时候是扔出一个MQ,异步消费了。如果没有资源竞争,例如库表秒杀,那么其实你确实很难接触多并发编程以及锁的使用。
但!凡有例外,比如你需要开发一个数据库路由中间件,那么就肯定会出现在一台应用实例上分配数据库资源池的情况,如果出现竞争就要合理分配资源。如此,类似这样的中间件开发,就会涉及到一些更核心底层的技术的应用。
所以,有时候不是没用
,而是你没有用。
二、面试题
谢飞机,小记!
线程我玩定了,面试也拦不住我,我说的!
谢飞机:嘿,你好哇,我是谢飞机!
面试官:好,今天电话面试,你准备好了?
谢飞机:准备好了,嘿嘿!
面试官:嗯,我看你简历里写了不少线程的东西,看来了解的不错。问你一个线程吧那就,线程之间状态是怎么转换的?
谢飞机:扒拉扒拉,扒拉扒拉!
面试官:嗯,还不错。那 yield 方法是怎么使用的。
谢飞机:嗯!好像是让出CPU。具体的没怎么用过!
面试官:做做测试,验证下,下次问你。
三、Thread 状态关系
Java 的线程状态描述在枚举类 java.lang.Thread.State
中,共包括如下五种状态:
这五种状态描述了一个线程的生命周期,其实这种状态码的定义在我们日常的业务开发中,也经常出现。比如:一个活动的提交、审核、拒绝、修改、通过、运行、关闭等,是类似的。那么线程的状态是通过下图的方式进行流转的,如图 20-1
New
:新创建的一个线程,处于等待状态。Runnable
:可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在 Runnable 中包含了Ready
、Running
两个状态,当线程调用了 start() 方法后,线程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是谦让的允许当前线程让出CPU,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争CPU,直至执行。Timed_waiting
:指定时间内让出CPU资源,此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleep
、Object.wait
、Thread.join
、LockSupport.parkNanos
、LockSupport.parkUntil
。Wating
:可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过 notify、notifyall 唤醒。下列方法都可以触发:Object.wait
、Thread.join
、LockSupport.park
。Blocked
:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如 synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。Terminated
:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。
*这里参考枚举State
类的英文注释了解了每一个状态码的含义,接下来我们去尝试操作线程方法,把这些状态体现出来。*
四、Thread 状态测试
1. NEW
这个状态很简单,就是线程创建还没有启动时就是这个状态。
2. RUNNABLE
创建的线程启动后
start()
,就会进入 RUNNABLE 状态。但此时并不一定在执行,而是说这个线程已经就绪,可以竞争 CPU 资源。
3. BLOCKED
这段代码稍微有点长,主要是为了让两个线程发生锁竞争。
第一个线程,synchronized 获取锁后休眠,不释放锁。
第二个线程,synchronized 获取不到锁,会被挂起。
那么最后的输出结果就会是,
BLOCKED
4. WAITING
只要在 synchronized 代码块或者修饰的方法中,调用
wait
方法,又没有被 notify 就会进入WAITING
状态。另外
Thread.join
源码中也是调用的 wait 方法,所以也会让线程进入等待状态。
5. Timed_waiting
有了上面状态获取的对比,这个状态的获取就没什么难度了。只要改成
Thread.sleep(100000);
就可以了。
6. Terminated
这个就比较简单了,只要一个线程运行完,它的生命周期结束了,就进入了
TERMINATED
状态。
五、Thread 方法使用
一般情况下 Thread 中最常用的方法就是 start 启动,除此之外一些其他方法可能在平常的开发中用的不多,但这些方法在一些框架中却经常出现。因此只了解它们的概念,但是却缺少一些实例来参考! 接下来我们就来做一些案例来验证这些方法,包括:yield、wait、notify、join。
1. yield
yield 方法让出CPU,但不一定,一定让出!。这种可能会用在一些同时启动的线程中,按照优先级保证重要线程的执行,也可以是其他一些特殊的业务场景(例如这个线程内容很耗时,又不那么重要,可以放在后面)。
为了验证这个方法,我们做一个例子:启动50个线程进行,每个线程都进行1000次的加和计算。其中10个线程会执行让出CPU操作。那么,如果让出CPU那10个线程的计算加和时间都比较长,说明确实在进行让出操作。
案例代码
测试结果
从测试结果可以看到,那些让出 CPU 的,执行完计算已经在10毫秒以上,说明我们的测试是效果的。
2. wait & notify
wait 和 notify/nofityall,是一对方法,有一个等待,就会有一个叫醒,否则程序就夯在那不动了。关于这部分会使用到的 synchronized
在之前小傅哥有深入的源码分析,讲到它是怎么加锁在对象头的,如果你忘记了可以翻翻看 《synchronized 解毒,剖析源码深度分析!》
接下来我们模拟鹿鼎记·丽春院,清倌喝茶吟诗聊风月日常。当有达官贵人来时,需要分配清倌给大老爷。中间会有一些等待、叫醒操作。只为让你更好的记住这样的案例,不要想歪喽。清倌人即是只卖艺欢场人,喊麦的。
案例代码
测试结果
效果效果主要体现 wait、notify,这两个方法的使用。我相信你一定能记住这个例子!
3. join
join 是两个线程的合并吗?不是的!
join 是让线程进入 wait ,当线程执行完毕后,会在JVM源码中找到,它执行完毕后,其实执行notify,也就是 等待
和 叫醒
操作。
源码:jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp
```c++
void JavaThread::exit(bool destroyvm, ExitType exittype) {
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
}
好的,就是这里!lock.notify_all(thread)
,执行到这,就对上了。
案例代码
测试结果
首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。
这部分验证的主要体现就是加了
thread.join()
后,会影响到输出结果。如果不加,main end!
会优先thread after
提前打印出来。join() 是一个 synchronized 方法,里面调用了 wait() 方法,让持有当前同步锁的线程进入等待状态,也就是主线程。当子线程执行完毕后,我们从源码中可以看到 JVM 调用了 lock.notify_all(thread) 所以唤醒了主线程继续执行。
六、总结
线程状态和状态的转换也是面试中必问的问题,但除了面试是我们自己在开发中,如果真的使用线程,是非常有必要了解线程状态是如何转换的。模模糊糊的使用,总会觉得担心,那么你是个好程序员!
线程的一些深入学习都是在调用本地方法,也就是需要了解到JVM层面,才能更加深刻的见到c++代码是如何实现这部分逻辑的。
在使用线程的时候一定要让自己有一个类似多核的脑子,线程一起、生死由你!本章节就扯到这了,很多的知识都是为了整套内容体系的全面,为后续介绍其他知识打下根基。感谢!
七、系列推荐
[Thread.start() ,它是怎么让线程启动的呢?](https://bugstack.cn/interview/2020/11/25/%E9%9D%A2%E7%BB%8F%E6%89%8B%E5%86%8C-%E7%AC%AC19%E7%AF%87-Thread.start()-%E5%AE%83%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%A9%E7%BA%BF%E7%A8%8B%E5%90%AF%E5%8A%A8%E7%9A%84%E5%91%A2.html)
版权声明: 本文为 InfoQ 作者【小傅哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/f67b43feab79c72a777b2dd3c】。文章转载请联系作者。
评论