前言
疫情期间看完了《深入理解Java虚拟机》一书,看完之后觉得有些囫囵吞枣,没有留下深刻印象,因此写点小小总结,把握一下重点知识,尽量形成认知框架中的一部分。
原书的内容十分详细,也肯定写得比我好。我这里只做简单的概括,详细的还是去书里看比较好。
Java内存模型
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。这里关注的主要是线程共有的实例字段、静态字段和构成数组对象的元素,它们会被存在主存中,以实现数据一致性。
虚拟机拥有的内存分为线程共有的主内存和线程私有的工作内存,Java内存模型规定所有变量都存储在主内存中,线程的工作内存保存有该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据
Java内存模型如下图所示:
内存操作
Java内存模型定义了8种操作来实现工作内存与主内存之间的交互,这8种操作都是原子的
double和long类型的变量,由于其需要占据多于1个变量槽,因此稍有区别
8种操作具体如下:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。
操作之间的先后执行顺序以及是否执行都有严格的约束,书上表述的十分详细,这里可以简单理解为:
- 先读后写:lock -> read -> load -> use -> assign -> store -> write -> unlock
volatile关键字
两个特性:
- 保证被修饰的变量对所有线程的可见性(必须要从内存中访问最新值)
- 禁止指令重排序优化(让线程更安全,比锁更轻量,但并不能保证线程安全)
先行发生原则
- 原子性:Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write
- 可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
- 有序性:
- 如果在本线程内观察,所有的操作都是有序的;(线程内似表现为串行的语义)
- 如果在一个线程中观察另一个线程,所有的操作都是无序的;(“指令重排序”现象和“工作内存与主内存同步延迟”现象)
为了保证多线程并发执行的安全性,Java有一个先行发生原则,它是判断数据是否存在竞争、线程是否安全的有效手段。有它的存在,一般的线程的安全得到保证
两个操作之间的”先行发生“关系,指定了两个操作之间的偏序关系,简单说开发者可以默认其发生的先后顺序,而无须再精细判断,虚拟机也无法对他们进行肆意排序。
Java内存模型中一些“天然的”先行发生关系如下:
- 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构
- 管程锁定规则(Monitor Lock Rule):一个
unlock
操作先行发生于后面对同一个锁的lock操作这里必须强调的是“同一个锁”,而“后面”是指时间上的先后
- volatile变量规则(Volatile Variable Rule):对一个
volatile
变量的写操作先行发生于后面对这个变量的读操作这里的“后面”同样是指时间上的先后
- 线程启动规则(Thread Start Rule):
Thread
对象的start()
方法先行发生于此线程的每一个动作 - 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测
可以通过
Thread::join()
方法是否结束、Thread::isAlive()
的返回值等手段检测线程是否已经终止执行 - 线程中断规则(Thread Interruption Rule):对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生可以通过
Thread::interrupted()
方法检测到是否有中断发生 - 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()
方法的开始 - 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那操作A先行发生于操作C
注意:时间先后顺序与先行发生原则之间基本没有因果关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准
Java线程
实现线程主要有三种方式:
- 使用内核线程实现(1:1实现)
- 使用用户线程实现(1:N实现)
- 使用用户线程加轻量级进程混合实现(N:M实现)
主流商用Java虚拟机的线程模型普遍都采用基于操作系统的原生线程模型来实现,即采用1:1的线程模型
- 缺陷:切换、调度成本高昂,系统能容纳的线程数量也很有限,不太适用于当今并发量超高的应用
线程调度方式:
- 协同式:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去
- 实现简单
- 切换线程对线程可知,且线程执行时间不稳定
- 抢占式:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
- 线程执行时间是系统可控的,切换线程对线程不可知
线程状态转换
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::parkUntil()
方法
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
线程状态转换的关系如下图所示: