最近一直在看多线程的一些知识, 看了一些书和一些博客, 收获还是挺多的, 最近看了java 并发编程的艺术这本书感觉收获很大也推荐给各位, 同时也结合以前看的博客就好好的总结一下自己所学的东西吧, 有不足的地方欢迎各位指正, 这篇文章主要是讲 volatile 关键字的知识.
volatile 的特性
可见性: volatile 在多线程中能够保证共享变量的 "可见性", 简单的说就是当一个线程修改了 volatile 变量的时候, java 线程内存模型能够确保所有的线程看到的这个变量的值是一致的.
防止指令重排序
java 内存模型
在学习 volatile 的知识之前我们先来简单了解下 java 内存模型 (JMM) 引用一张网上很经典的表示 java 内存模型的图
大概解释下这个图的意思
在多个线程运行的时候每一个线程 (Thread) 都有一个属于自己的内存空间
多个线程共同使用一个主存
每个线程在对数据进行修改之前都会先在主存里面获取相关数据, 然后在自己的工作内存里面对数据进行操作.
可见性
关键的地方就来了, 因为每个线程然都是在自己的内存里进行操作, 然而每个线程的工作内存之间都是相互不可见的, 所以对共享变量的修改并不会马上被其他线程看到, 所以就会造成多个线程操作同一个数据但是最后结果并不是我们期望的结果. 当线程 1 去首先从主存中加载一个 volatile 变量到自己的工作内, 然后对这个 volatile 变量进行写操作, 写入操作结束之后, volatile 变量的最新值会立马刷新到主内存, 同时其他线程中的这个 volatile 变量会立马失效, 会被强迫从主内存中重新读取 volatile 变量的最新值, 这就是 volatile 变量的可见性实现的过程. 同时这也可以看做是一个线程和其他线程通信的一个过程.
防止指令重排序
简单解释下指令重排序, 重排序指的是编译器和处理器为了优化程序的性能会对指令序列进行重新排序的一种手段. 在单线程的程序里, 指令的重排序会保证执行结果的正确性, 但是在多线程中指令的重排序对程序的执行结果的正确性就得不到保障(指令重排序的一些规则各位可以去查阅一下, 这里不赘述).volatile 变量防止指令重排序请先看下面的内存屏障介绍.(其实 synchronized 也对指令的重排序有一些限制, 在后面我会写一篇讲解 synchronized 的博文会详细讲解一下, 欢迎各位阅读)
内存屏障
JMM 把内存屏障分为四类(摘自 Java 并发编程艺术)
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Loadl; LoadLoad; Load2 | 确保 Loadl 数据的装载先于 Load2 及所有后续装载指令 |
StoreStore Barriers | Storel; StoreStore; Store2 | 确保 Store1 数据对其他处理器可见 (刷新到内存) 先于 Store2 及所有后续存储指令的存储 |
LoadStore Barriers | Loadl; LoadStore; Store2 | 确保 Loadl 数据装载先于 Store2 及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Storel; StoreLoad; Load2 | 确保 Storel 数据对其他处理器变得可见 (指刷新到内存) 先于 Load2 及所有后续装载指令的装载。StoreLoad Barriers 会使该屏障之前的所有内存访问指令 (存储和装载指令) 完成之后,才执行该屏障之后的内存访问指令 |
volatile 变量基于保守策略的 JMM 内存屏障插入策略
在每个 volatile 写操作的前面插入一个 StoreStore 屏障.
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障.
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障.
在每个 volatile 读操作的后面插入一个 LoadStore 屏障.
看完这个相信各位对 volatile 防止指令重排序就有一个比较清楚的认识了, 解释一下上面的四条策略,
在 volatile 变量的写操作前面的其他写操作会在 volatile 变量写前面执行(提前刷新到主存, 对其他线程可见)
volatile 变量的写会比后面的其他读写操作先进行
volatile 变量读操作前面的读操作会在 volatile 变量读操作以前进行
volatile 变量读操作后面的其他写操作会在 volatile 变量读操作以后进行
来源: https://juejin.im/post/5b1d13ce5188257d7541be5e