1 Java与线程
- 目前线程是Java里面进行处理器资源调度的最基本单位。如果日后Loom项目能够为Java引入纤程(Fiber)的话,可能会改变这一点。
1.1 线程的实现
- 这里先把Java技术的背景放下,以一个通用的应用程序的角度来看线程是如何实现的。
1.1.1 内核线程实现
- 利用内核线程实现的方式也被称为1:1实现。内核线程是直接由操作系统支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
- 每个内核线程都可以视为一个内核的分身,这样子操作系统就有能力同时处理多件事情,
支持多线程的内核就称为多线程内核
。 - 一般不会直接使用内核线程,而是使用内核线程的一种高级接口——
轻量级进程
,轻量级进程就是我们通常意义上讲的线程,每个轻量级进程都由一个内核线程支持。但是使用内核线程,所有的线程操作都需要交给系统调用,而系统调用的代价是相对较高的,需要在用户态和内核态中来回切换。
1.1.2 用户线程实现
- 用户线程实现的方式被称为1:N实现,广义上来讲,
一个线程只要不是内核线程,都可以认为是用户线程。
从定义上说,轻量级进程也属于用户线程,但是因为它的实现始终是建立在内核上的,通常意义上并不具备用户线程的优点。 狭义的用户线程
是建立在用户空间的线程库上的,系统不能感知到用户线程的存在。用户线程的建立、同步、销毁和调度完全在用户态中完成。 如果程序实现得当,这种线程不需要切换到内核态,因此操作非常快速且低耗的。- 但是困难在于用户需要自己处理所有的线程操作,有些问题会非常难以处理,Java语言曾经也使用过,但是还是放弃了。
1.1.3 混合实现
- 将内核线程和用户线程结合起来使用的方式,被称为N:M实现
- 用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。
- 操作系统支持的轻量级进程作为用户线程和内核线程之间的桥梁,
这样可以使用内核提供的线程调度功能和处理器映射
。大大降低了整个进程被完全堵塞的风险。
1.2 Java线程调度
- 线程调度是指系统为线程 分配处理器使用权的 过程。
- 调度方式主要有两种:协同式线程调度和抢占式线程调度
1.2.1 协同式调度的多线程系统
- 线程的执行时间由线程本身来控制,线程把自己的工作做完之后,要主动通知系统切换到另一个线程上去。
- 好处:实现简单。切换操作对线程可知,一般没有线程同步问题。
- 坏处:执行时间不可控,如果一个线程的代码编写有问题,那么不会告知系统进行线程切换,就会一直阻塞在那里。
1.2.2 抢占式调度的多线程系统
- 每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
- Java中,通过Thread::yield()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程是没有什么办法的。
- 线程的执行时间是系统可控的,所以不会有因为一个进程出问题,因为一个线程导致整个进程出了问题。
1.2.3 Java优先级
- Java的线程调度是系统自动完成的,但是我们可以建议操作系统给某些线程多分配一些时间。这项操作可以通过设置线程优先级来完成。优先级越高的线程越容易被系统选择执行。
状态转换
- Java语言自定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。
- 新建(New):创建后尚未启动的线程处于这种状态
- 运行(Runnable):包括操作系统中的Running和Ready,也就是处于此状态的线程有可能在执行,也有可能在等待着操作系统为它分配时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配 处理器执行时间,它们要等待被其他线程显示唤醒。
- 没有设置Timeout参数的Object::wait()方法
- 没有设置Timeout参数的Thread::join()方法
- LockSupport::park()
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配 处理器执行时间,不过无须等待被其他线程显示唤醒,在一定时间之后,系统会自动唤醒。
- Thread::sleep()方法
- 设置Timeout参数的Object::wait()方法
- 设置Timeout参数的Thread::join()方法
- LockSupport::parkNanos()方法
- LockSupport::pakUntil()方法
- 阻塞(Blocked):线程被阻塞了的,"阻塞状态"与"等待状态"的区别是阻塞状态在等待着获取一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生。 而等待状态是等待一段时间,或者唤醒动作的发生。
- 结束(Terminated):已经终止线程的线程状态,线程已经结束执行。
2 Java与协程
- 如今的请求越来越多,请求本身的执行时间变得很短,数量变得很多。
- 但是因为Java目前的多线程机制就是采用内核线程模型,所以说线程之间的切换就会非常的频繁,这会造成严重的浪费。内核线程调度的成本主要来自于用户态与核心态之间的状态转换,而这两者之间的开销来自于响应中断、保护和恢复执行现场的成本。
- 当中断发生时,线程A切换到线程B之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程b挂起时候的状态,这样线程B被重新激活后才能仿佛从来没有挂起。
这种保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中来回的拷贝,当然不可能是一种轻量级的操作。