★
上一章讲解了 JMM 架构和存在意义,我们知道,JMM 其实是为多线程而生,多线程并发不得不围绕三点展开:原子性、可见性、有序性,本章就围绕这三大特性来分析一下 JMM 的具体设计
”
1.原子性(Automicity)
由 Java 内存模型来直接保证原子性的变量操作包括 read、load、use、assign、store、write 这 6 个动作,虽然存在 long 和 double 的特例,但基本可以忽律不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock 和 unlock 也可以满足需求。lock 和 unlock 虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令 monitorenter 和 monitorexit 对应这两个操作,对应到 java 代码就是 synchronized 关键字,因此在 synchronized 块之间的代码都具有原子性。
看下面一个例子,请分析以下哪些操作是原子性操作:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
咋一看,有些朋友可能会说上面的 4 个语句中的操作都是原子性操作。其实只有语句 1 是原子性操作,其他三个语句都不是原子性操作。
语句 1 是直接将数值 10 赋值给 x,也就是说线程执行这个语句的会直接将数值 10 写入到工作内存中。
语句 2 实际上包含 2 个操作,它先要去读取 x 的值,再将 x 的值写入工作内存,虽然读取 x 的值以及 将 x 的值写入工作内存 这 2 个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1 包括 3 个操作:读取 x 的值,进行加 1 操作,写入新的值。
所以上面 4 个语句只有语句 1 的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
2.可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。
主要有有三种实现可见性的方式:
volatile:volatile 类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。
synchronized:synchronized 关键字是通过 unlock 之前必须把变量同步回主内存来实现的。
final:final 则是在初始化后就不会更改,所以只要在初始化过程中没有把 this 指针传递出去也能保证对其他线程的可见性。
3.有序性
有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
前半句说的就是“线程内表现为串行的语义”,后半句值得是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。
保证有序性的关键字有 volatile 和 synchronized,volatile 禁止了指令重排序,而 synchronized 则由“一个变量在同一时刻只能被一个线程对其进行 lock 操作”来保证。
4.总结
synchronized 对三种特性都有支持,十分好用,但是性能稍微差一点,大量使用并发很差
volatile 可以说是 java 虚拟机中提供的最轻量级的同步机制。java 内存模型对 volatile 专门定义了一些特殊的访问规则。他能保证多线程中变量对所有线程的可见性,volatile 类型的变量禁止指令重排序优化