中年|揭秘JMM、Synchronized、Volatile之间的关系( 二 )



如果一个变量事先没有被lock操作锁定 , 那就不允许对它执行unlock操作 , 也不允许去unlock一个被其他线程锁定的变量 。
对一个变量执行unlock操作之前 , 必须先把此变量同步回主内存中(执行store、write操作) 。
这8种内存访问操作以及上述规则限定明确地描述了在我们Java程序中哪些内存访问操作在并发下是安全的 。 但是这样操作却是极其繁琐的 , 所以被简化成了read , write , lock , unlock四种操作 , 但也只是语言上的简化 , 实际模型的基础设计并未简化 。
1.3 JMM的特性
我们在开头提到了volatile的三大特性 , 然后要介绍就要先普及JMM的基础 , 其实并不是没有道理的 。 关于JMM我们也有三大特性(JMM保证) , 总结起来就是:
原子性
可见性
有序性
可以发现 , 这与volatile是很类似的 。 下面一张图可以很好的理清之间的关系:
中年|揭秘JMM、Synchronized、Volatile之间的关系
本文插图

在上面基本的数据类型读或写 , 我们看到了long , double除外 , 这涉及到了针对long和double型变量的特殊规则 。
Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性 , 但是对于64位的数据类型(long和double) , 在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行 , 即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性 , 这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables) 。
关于三大特性 , 我们这里简单介绍一下
1.3.1 原子性
原子性呢是指操作是一体的 , 要么成功 , 要么失败 , 没有第三种情况 。 上图我们也说到Java中的基本数据类型的访问 , 读或写都是具备原子性的 。 这里我们举一个例子
int i = 5
这里我们的赋值操作就是原子性 , 而一个比较经典的就是
int i = 0i++
这里i++就不是原子性的 , 我们可以看作它是先获取了i的值 , 然后进行写入值i = 1的操作 。 我们常见的数据类型的操作都是原子性的 , 但是如果应用场景需要一个更大范围的原子性保证的话 , Java内存模型提供了lock和unlock操作来满足这种需求 。 也提供了更方便快捷的synchronized关键字 , 同样具备原子性 。
1.3.2 可见性

可见性就是指当一个线程修改了共享变量的值时 , 其他线程能够立即得知这个修改 。 比如当我们没有任何操作来处理两个线程的时候:
int i = 0 //主线程i++ //线程1j = i //线程2
我们可以发现线程1修改了i的值 , 但是没有刷回主内存 , 线程2读取了i的值 , 赋值给了j , 我们期望j就是1 , 但是因为虽然线程1修改了 , 没有来得及复制到主内存中 , 线程2读取后 , j还是0 。 这就是内存不可见性 , 同理我们就可以理解了可见性 。
常见的我们可以用volatile关键字来修饰变量 , 达到了内存可见性 。
Java内存模型是通过在变量修改后将新值同步回主内存 , 在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的 , 无论是普通变量还是volatile变量都是如此 。 普通变量与volatile变量的区别是 , volatile的特殊规则保证了新值能立即同步到主内存 , 以及每次使用前立即从主内存刷新 。 因此我们可以说volatile保证了多线程操作时变量的可见性 , 而普通变量则不能保证这一点 。除了volatile之类 , 还有synchronized和final也可以保证可见性 。 syschronized是因为JMM的规则限定对一个变量执行unlock操作之前 , 必须先把此变量同步回主内存中(执行store、write操作) , 而final关键字的可见性是指:


推荐阅读