深入学习 Java 的线程
线程的状态/生命周期
使用 jstack 可以查看线程的状态
Java中线程的状态分为6种
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
状态之间的变迁图示:
其他的线程相关方法
yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。
wait()/notify()/notifyAll()
线程的优先级
在Java线程中,setPriority(int 1~10)方法来修改优先级,默认优先级是5。
在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
线程的调度
线程调度是指系统为线程分配CPU使用权的过程,主要调度方式有两种:
协同式线程调度(Cooperative Threads-Scheduling)
抢占式线程调度(Preemptive Threads-Scheduling)
Java线程调度就是抢占式调度
线程和协程
任何语言实现线程主要有三种方式:
- 使用内核线程实现(1:1实现)
- 这种线程由内核来完成线程切换, 内核通过操纵调度器(Scheduler) 对线程进行调度, 并负责将线程的任务映射到各个处理器上。
- 局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、 析构及同步,都需要进行系统调用。而系统调用的代价相对较高, 需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个语言层面的线程都需要有一个内核线程的支持,因此要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持的线程数量是有限的。
- 使用用户线程实现(1:N实现)
- 严格意义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
- 用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援, 所有的线程操作都需要由用户程序自己去处理。
- 使用用户线程加轻量级进程混合实现(N:M实现)
- 用户线程还是完全建立在用户空间中, 因此用户线程的创建、 切换、 析构等操作依然廉价, 并且可以支持大规模的用户线程并发。
- 同样又可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过内核线程来完成。
系统调用,触发系统上下文切换
Java线程的实现
以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构, 所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理。
所以,这就是我们说Java线程调度是抢占式调度的原因。
协程
- 出现的原因
- 微服务的兴起
- 协程简介
- 内核线程的切换开销是来自于保护和恢复现场的成本
- 由于最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling) 的,所以它有了一个别名——“协程”(Coroutine) 完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”(Stackfull Coroutine)。
- 协程的主要优势是轻量, 无论是有栈协程还是无栈协程, 都要比传统内核线程要轻量得多。
- 协程机制适用于被阻塞的,且需要大量并发的场景(网络io),不适合大量计算的场景。
- 纤程-Java中的协程
- 目前Java中比较出名的协程库是Quasar[ˈkweɪzɑː(r)], Quasar的实现原理是字节码注入,在字节码层面对当前被调用函数中的所有局部变量进行保存和恢复。
- JDK19的虚拟线程
- 2022年9月22日,JDK19(非LTS版本)正式发布,引入了协程,并称为轻量级虚拟线程。
- 使用javac --release 19 --enable-preview XXX.java编译程序,并使用 java --enable-preview XXX 运行该程序
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。
调用Thread.setDaemon(true)将线程设置为Daemon线程。
线程间的通信和协调、协作
管道输入输出流
Java的线程里也有类似的管道机制,用于线程之间的数据传输,而传输的媒介为内存。
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
try {
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
/*将键盘的输入,用输出流接受,在实际的业务中,可以将文件流导给输出流*/
while ((receive = System.in.read()) != -1){
out.write(receive);
}
} finally {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
join方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
Thread productService = new Thread(new GetProduct());
Thread userService = new Thread(new GetUser());
Thread formatService = new Thread(new FormatService());
//依次执行
productService.start();
productService.join();
userService.start();
userService.join();
formatService.start();