在多线程环境下,线程之间的通信,就不得不提 JMM (java 内存模型)
在 JVM 内部使用的 java 内存模型 (JMM) 将线程堆栈和堆之间的内存分开
线程堆栈 (thread stack):
1. 运行在 java 虚拟机上的每个线程都有自己的线程堆栈 (thread stack)
2. 线程堆栈还包含正在执行的每个方法的所有局部变量,一个线程只能访问它自己的线程堆栈。由线程创建的局部变量对于除创建它的线程之外的所有其他线程都是不可见的。
3. 即使两个线程正在执行完全相同的代码,两个线程仍然会在每个线程堆栈中创建该代码的局部变量,一个线程可能会将一个有限变量的副本传递给另一个线程,但它不能共享原始局部变量本身
堆:
1. 堆包含在 Java 应用程序中创建的所有对象,而不管是不是由线程创建的该对象。
2. 堆中的对象可以被具有对象引用的所有线程访问。当一个线程访问一个对象时,它也可以访问该对象的成员变量。
3. 如果两个线程同时调用同一个对象上的一个方法,它们都可以访问该对象的成员变量,但每个线程都有自己的局部变量副本
4. 堆中的数据是共享的,线程不安全的
Java 内存模型带来的问题
可见性问题
CPU 中运行的线程从主存中拷贝共享对象 obj 到它的 CPU 缓存,把对象 obj 的 count 变量改为 2。但这个变更对运行在右边 CPU 中的线程不可见,因为这个更改还没有 flush 到主存中:要解决共享对象可见性这个问题,我们可以使用 java volatile 关键字或者是加锁
内存交互操作
内存交互操作有 8 种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
load (载入):作用于工作内存的变量,它把 read 操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用
write (写入):作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中
JMM 对这八种指令的使用,制定了如下规则:
不允许 read 和 load、store 和 write 操作之一单独出现。即使用了 read 必须 load,使用了 store 必须 write
不允许线程丢弃他最近的 assign 操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有 assign 的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施 use、store 操作之前,必须经过 assign 和 load 操作
一个变量同一时间只有一个线程能对其进行 lock。多次 lock 后,必须执行相同次数的 unlock 才能解锁
如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值。
如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
对一个变量进行 unlock 操作之前,必须把此变量同步回主内存
JMM 对这八种操作规则和对 volatile 的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用 java 的 happen-before 规则来进行分析。
Volatile 是 Java 虚拟机提供的轻量级的同步机制
保证可见性
不保证原子性
指令重排
关于 JMM 的一些同步约定:
线程解锁前,必须把共享变量立刻刷回主存
线程加锁前,必须读取主存中的最新值到工作内存中
加锁和解锁是同一把锁
线程:工作内存、主内存