目录
一、什么是Java内存模型?
Java内存模型描述了多线程的执行规则。
二、Java内存模型 vs JVM运行时数据区
三、了解CPU的指令重排
Java编程语言的语义允许Java编译器和微处理器进行执行优化,这些优化就导致了与其交互的代码不再同步,
从而导致看似矛盾的行为。
比如说:
while(isRuning){
i++;
}
上述这段代码,在字节码中,会先得到 isRuning 的值——> 判断 isRuning 的值——> i++ ——>跳回第一行。
四、可见性问题
什么是可见性问题?
有一个值存在于内存中,有一条线程对该值进行写的操作,另一条线程进行读取却读不到;这就是可见性问题。
请看如下代码,最终控制台上会打印什么样的信息??
public class Test {
int i = 0;
boolean isRunning = true;
public static void main(String[] args) throws Exception {
Test test = new Test();
new Thread(() -> {
while (test.isRunning) {
test.i++;
}
System.out.println("i==" + test.i);
}).start();
LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
test.isRunning = false;
System.out.println("shutdown....");
}
}
通过执行实例代码,我们可以发现,控制台只打印了一句:shutdown...
为什么呢?这就涉及了可见性问题(以及指令重排)。
因为这涉及到了CPU的缓存,CPU共有三层缓存,这里,只讲CPU的高速缓存;
问题分析:
1、CPU的高速缓存:
主线程休眠3秒前,次线程读取到的 isRuning=true,此时进入循环体,等到主线程修改
isRuning=false时,该值还是处在CPU的高速缓存中,没有及时更新到堆内存;所以导致
次线程读取的 isRuning 还是 true。
反过来分析也是一样的,就是说主线程已经更新了值,但次线程读取的还是当前线程在CPU高速缓存中的值。
或者说,主线程修改的值还在高速缓存中,同时次线程读取的也是告诉缓存中的值。
2、JVM的指令重排
while循环体的代码会被指令重排,重排后代码如下:
boolean f = test.isRunning;
if (f){
while(true){
test.i++;
}
}
整体代码分析图解:
五、volatile关键字
可见性问题:让一个线程对共享变量的修改,能够及时的被其它线程看到。
根据JMM规定的happen before 和同步原则:
对于某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作;
对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步。
也就是说,对一个变量的修改,在后续的访问中,一定能读取到正确的值。
要满足这些条件,所以 volatile 关键字就有这些功能:
1、禁止缓存:
volatile 变量的访问控制符会加个 ACC_VOLATILE
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
2、对 volatile 修饰的变量相关的指令不做重排序。
六、线程间操作的定义
1、线程间操作是指:一个程序执行的操作可被其它线程感知,或被其它线程直接影响;
2、Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按线程内的语义执行。
七、同步的规则定义
- 对于对 volatile 变量 v 的写入,与所有其它线程后续对 v 的读同步;
- 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步;
- 对于每个属性写入默认值(o,false,null)与每个线程对其进行的操作同步;
- 启动线程的操作与线程中的第一个操作同步;
- 线程T2的最后的操作与线程T1发现线程T2已经结束同步;(isAlive,join可以判断线程是否终结)
- 如果线程T1中断了T2,那么线程的T1中断操作与其他所有线程发现T2被中断同步;
通过抛出InterruptedException异常,或者调用Thread.interrupted或者Thread.isInterrupted。
同步:即可见,也就是不会被缓存。
八、Happens-before先行发生原则
happens-before关系用于描述两个有冲突的动作之间的顺序,如果一个action happens-before
另一个action,则第一个操作可被第二个操作可见;JVM需要实现如下happens-before规则:
- 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作;
- 某个线程上的 unlock 动作都 happens-before 同一个线程上后续的 lock 动作;
- 对某个 volatile 字段的写操作 happens-before 每个后续对 volatile 字段的读操作;
- 在某个线程对象上调用 start() 方法 happens-before 被启动线程中的任意操作;
- 如果在线程 t1 中调用了 t2.join() ,那么 t2 中的所有操作对 t1 可见;
- 如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c ,那么存在 a happens-before c;
九、final 在JVM 中的处理
1) 如果 final 修饰了对象的字段,那么当线程看到该对象时,读取到的字段值一定是最新的;
2) 通常被 static final 修饰的字段,不能被修改。然而 System.in 、System.out、System.err 被final 修饰,却
可以被修改,这个是遗留问题,必须允许通过 set 方法改变,我们将这些字段称为写保护,以区别于普通
的 final 字段。
十、Word Tearing 字节处理
有些处理器(尤其是早期的Alphs处理器)没有提供写单个字节的功能。在这样的处理器上更新 byte 数组,若
只是简单地读取整个内容,更新对应的字节,然后将整个内容写回内存,将是不合法的。
这个问题有时候被称为 “字节分裂” ,更新单个字节有难度的处理器,就需要寻找其它方式解决问题,因此,
编程时需要注意,尽量不要对 byte[] 中的元素进行重新赋值,更不要在多线程中这样做。
十一、double 和 long 的特殊处理
—> 由于《Java语言规范》的原因,对非 volatile 的 double、long 的单次写操作是分两次进行的,每次操作其中的
32位,这就可能导致第一次写入后,读取时,读取的是脏数据,等到第二次写完成之后,才能读取到正确值。
—> 读写 volatile 修饰的 double、long 是原子性的。