Volatile作用
1. 保证线程可见性
-
多线程之间共享的变量是存放在主内存(堆内存)中的,线程运行时,把主内存中的变量复制一份到自己的工作区,之后在线程执行的过程中就使用自己工作区中的副本了,如果这时其他线程对主内存中的变量进行了修改,当前线程可能无法获取到最新的值。
以下三种情况除外,当线程代码块中存在下面的代码时,会重新从主内存同步变量值
- 当前线程中使用了System.out.println()进行输出
- 线程进行sleep
- 线程中进行文件操作
-
问题:变量在工作内存中改变了之后会立刻同步到主内存中吗?
Java内存模型规定必须满足如下规则:- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 所以,线程修改工作区变量之后应该会立刻同步到主内存
-
使用volatile关键字,当一个线程修改了共享变量的值,其他线程能够立刻看到修改后的值,保证了线程之间的可见性
-
volatile的底层实现
基于cpu的缓存一致性协议MESI
Java内存模型(Java Memory Model JMM)相关文章 https://segmentfault.com/a/1190000022564037 https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
在个人电脑实验过程中,发现加不加volatile关键字好像没什么区别。但是在单位的台式机上同样的代码,加不加volatile又会有很大的区别。
一时比较疑惑,有人说是虚拟机在不同处理器上对工作内存回写主内存的速度有较大差异。感觉也不太对,因为修改工作区变量的值后会同步到主内存中,速度可能有差异,但是感觉区别是在其他线程什么时候去读主内存的最新值上。待考证。
2. 禁止指令重排序
单例设计模式中的双重检查写法
public class L01_Singleton {
// 是否需要用volatile修饰
private static /*volatile*/ L01_Singleton s;
// 电脑数量
private int computerNum = 0;
public /*synchronized*/ int getComputerNum(){
return computerNum;
}
public synchronized void setComputerNum(){
for (int i = 0; i < 10000; i++) {
computerNum++;
}
}
private L01_Singleton(){}
public static L01_Singleton getInstance(){
// 双重检查模式,防止多线程情况下出现创建多个对象的情况
if (s==null){
synchronized (L01_Singleton.class){
if(s == null){
s = new L01_Singleton();
}
}
}
return s;
}
public static void main(String[] args){
int count = 0;
for (int i = 0; i <100 ; i++) {
new Thread(()->{
L01_Singleton computer = L01_Singleton.getInstance();
computer.setComputerNum();
},"t"+i).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(L01_Singleton.getInstance().getComputerNum());
}
}
问题:在定义静态的对象属性时,是否需要使用volatile修饰?
- 需要使用,因为jvm在进行编译的过程中会进行指令重排序。
- 以new一个对象为例,会被编译成以下主要的三条指令[Object o = new Object()]
- 申请堆内存(实例化)为属性设置默认值
- 对象初始化(1、属性填充 2、初始化方法)
- 将申请的堆内存地址赋值给栈内存中的引用
0 new #2 <java/lang/Object> //申请并分配内存 1 3 dup //复制栈顶数值,并且复制值进栈 4 invokespecial #1 <java/lang/Object.<init>> //初始化 2 7 astore_1 //将申请的堆内存地址赋值给栈内存中的引用 3
- 如果编译的过程中将第三条指令和第二条指令的顺序颠倒了,变成了1->3->2,这时就会存在问题,具体是什么问题呢?
- 线程1执行到第一条指令和重排序后的第二条指令(赋值指令)
- 线程2执行,获取实例时发现引用不为空,直接获取。这时读到的是属性的默认值,比如说是0。接着对属性进行修改,比如说进行+1操作,这时属性的值就会被置为1。
- 如果是在并发量特别高的情况下,线程2执行的过程中,可能其他线程已经将数字改为20了,这时线程2将数字置为1就会出现大问题。比如说秒杀100台电脑,将数字重置为1,相当于多出了20台电脑参与了秒杀。
3.特点
- volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
- volatile只能保证可见性,不能保证原子性。因此volatile不能替代synchronized
- synchronized可以保证可见性和原子性