在现代的操作系统中,线程(Thread,又称轻量级进程)是操作系统最小的调度单元,每个线程都会维护着属于自己的计数器(Program Count,PC)、栈(Stack)等等属性。处理器在这些线程之间高速切换,让使用者以为这些线程在同时执行。
线程基础
为什么使用多线程
- 现在主流处理器都是多核的,单核的处理性能极其有限。如果上层应用只用到了一个核,其余核就闲置在那,资源利用率就低;如果上层应用在正确的场景、正确的配置下使用到了多个核,那么程序的性能会得到大幅度提升
- 在一些场景下,操作和操作之间并不是强依赖性的,可以让两者同时进行,提高效率,缩短响应时间
当然,如果在不正确的场景下应用多线程,可能会因为频繁的上下文切换,导致性能下降。这里举两个场景:
- 正确的场景:要结合很多计算一个值,我们可以将数据切片分到不同的线程中计算,最后汇总结果
- 不正确的场景:对同一个资源进行修改,如果是多线程环境下,可能会因为锁、上下文切换等原因降低速度
总而言之,多线程不是银弹,仍然要因地制宜。
线程优先级
现代操作系统会分出一个个时间片给线程,线程拿到的时间片越多,执行时间越长;一旦线程的时间片用完了,该线程也就要面临线程调度,等待着下一次的分配。而优先级( Priority
)就是决定了操作系统愿不愿意分更多的时间片给某个线程的因素了。Java
可以通过 Thread#setPriority()
设置线程的优先级~优先级越高分配到的时间片越多;反之分配到的时间片越少。
然而这个功能吧,在 Windows
、 Ubuntu
这些操作系统上都是摆设~
线程状态及变更
JVM线程状态及变更
在 JVM
里,线程的状态有以下几种:
状态名称 | 解释 |
---|---|
NEW | 当线程刚刚创建,还没有调用 start() 时 |
RUNNABLE | 运行中状态, Java 合并了操作系统中“就绪”和“运行”两种状态,统称为“运行中” |
WAITING | 等待状态,表示当前线程处于等待状态,需要其他线程执行一些特定的动作来唤醒该线程 |
TIME_WAITING | 超时等待状态,表示当前线程处于等待状态,但是可以在一段时间后自行返回(解除等待),同样也可以被其他线程唤醒 |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
TERMINATED | 终止状态,表示线程已经执行完毕 |
其状态变更图如下所示:
当线程刚被创建时处于 NEW
状态;当调用 Thread#start()
时,线程状态会进入 RUNNABLE
,这其中如果分配到了时间片就进入 RUNNING
状态,没有分到就处于 READY
状态,这两个状态在 JAVA
中统称为 RUNNABLE
;如果此时调用了类似 wait()
、 join()
等没有指定时间的方法,线程就会进入 WAITING
状态;如果调用的是 wait(time)
、 join(time)
、 sleep(time)
等指定了时间的方法,线程就会进入 TIME_WAITING
状态;如果线程之间在争夺锁,没有获取到锁的线程就会进入 BLOCKED
状态;最后,线程执行完毕后,就会进入 TERMINATED
状态。
然鹅到这里并没有结束, JVM
是虚拟机,也是个程序,这上面的线程最终是会被操作系统管理起来的,那么在操作系统上,这些线程状态是怎样的呢?
操作系统线程状态及变更
在操作系统中基本就涉及三种状态:
状态名称 | 解释 |
---|---|
READY | 线程已经被创建,正在等待系统调度分配CPU使用权。 |
RUNNING | 线程获得了CPU使用权,正在进行运算 |
BLOCKED(WAITING) | 线程等待(或者说挂起),让出CPU资源给其他线程使用 |
其状态变更示意图如下所示:
点击查看【processon】
当线程刚刚创建出来时,通常处于 READY
状态等待系统分配时间片,当线程获取到时间片后会进入 RUNNING
状态;当线程因为读取 IO
或者获取某些临界资源失败时,会进入 BLOCKED
状态;当 BLOCKED
线程被唤醒时又会进入 READY
状态等待。
两者的关系
这里特别要注意,当 JVM
层面发生了阻塞 IO
时,它会处于操作系统的 BLOCKED
状态,但在 JVM
上显示的是 RUNNABLE
:
点击查看【processon】
总而言之,在 JVM
里的 BLOCKED
、 WAITING
、 TIMED_WAITING
都属于操作系统层面的 BLOCKED
;而发生 IO
时, JVM
层面虽然还是 RUNNABLE
,但实际操作系统里是属于 BLOCKED
的。
在《Java 多线程编程实战指南-核心篇》里有这么一段话:
从Java应用的角度来看,一个线程的生命周期状态在RUNNABLE状态与非RUNNABLE状态(包括BLOCKED、WAITING和TIMED**_WAITING中的任意一个子状态)之间切换的过程就是一个上下文切换的过程。当一个线程的生命周期状态由**RUNNABLE转换为非RUNNABLE时,我们称这个线程被暂停。…而一个线程的生命周期状态由非**RUNNABLE状态进入RUNNABLE状态时,我们就称这个线程被唤醒。一个线程被唤醒仅代表该线程获得了一个继续运行的机会,而并不代表其立刻可以占用处理器运行。因此,当被唤醒的线程被操作系统选中**占用处理器继续其运行的时候,操作系统会恢复之前为该线程保存的上下文,以便其在此基础上进展。
就图来看,JVM层状态发生改变,操作系统一定发生了上下文切换;但是操作系统发生上下文切换,不一定引起JVM层状态变换。
线程类型
在 Java
里,线程分为两种:
- 守护线程(
Daemon Thread
,为啥不是Deamon
) - 用户线程(
User Thread
)
当 JVM
上的所有线程都仅剩守护线程时,不管守护线程执行到哪里了, JVM
都会自动退出。如果有任一用户线程还存在, JVM
就不能退出!
在 Java
里守护线程的设置是通过 Thread#setDaemon(bool)
来设置的,必须得在线程启动前执行;否则会抛出 IllegalThreadStateException
异常。
必知必会
线程创建与销毁
线程间通讯
拓展
Java里为何合并就绪和运行状态?
现在的时分多任务操作系统都是使用的时间分片方式进行抢占式论转调度。这个时间片通常很小,一个线程在时间片上通常只有10ms~20ms(运行中状态),消耗完时间片又要进入调度队列等待调度(就绪状态)。通常Java的线程状态就是拿来监控服务的,如果线程状态切换的如此之快,那么区分”运行中“和”就绪“就没多大意义了。
简单来说,就是 Java
中线程状态的用途通常是为了观测业务,是 RUNNING
还是 READY
其实不那么重要,所以 Java
模型中合并 RUNNING
和 READY
状态,统称为 RUNNABLE
。