14volatile(轻量级的同步机制)
Posted zxhbk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了14volatile(轻量级的同步机制)相关的知识,希望对你有一定的参考价值。
谈谈你对 volatile 的理解
Volatile 是 Java 虚拟机提供的轻量级的同步机制
它的3个特性:
1、保证可见性
2、不保证原子性(原子性就是任务要么完整执行,要么都不执行)
3、禁止指令重排
1、保证可见性
-
上面代码中程序不是死循环了吗?因为线程A并不知道num的值已经被修改。
-
如何解决呢?
-
因为volatile关键字保证了可见性,所以主存中的值发生修改后,其他线程可以清晰地看到。
package com.zxh.testValidate; import java.util.concurrent.TimeUnit; public class Demo01 { // volatile 保证可见性,但主存的值发生修改,线程可以看见 private volatile static int num = 0; // 内存的变量 public static void main(String[] args) { // 线程A不断对num值进行访问 new Thread(()->{ while(num == 0){ } }).start(); // 为了保证线程A先启动,进行延迟1s,否则会导致num直接=1 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // main线程在另一线程还在运行时,将num变为1 num = 1; System.out.println(num); // 输出是否被修改 } }
2、不保证原子性
原子性:不可分割
比如:当线程A在执行时,不能被打扰,要求操作全部都完成,不能被分割。要么同时完成,要么同时失败;
举例
- 下面:开了20个线程,每个线程执行了1000次对 num + 1操作。
package com.zxh.testValidate; public class Demo02 { private static int num = 0; // 定义一个变量 public static void add(){ num++; // 对num进行+1操作 } public static void main(String[] args) { // 理论情况,num应该变成了20000 for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行 while(Thread.activeCount() > 2){ Thread.yield(); // 礼让其他线程先执行 } // 当其他线程执行完add()操作后,再输出结果 System.out.println("num:" + num); } }
但是发现 num 并没有加到 20000,为什么呢?
分析问题的原因
分析线程的执行操作,解答这个问题
1、打开生成的 target 目录下的,这个执行文件所在的位置
正如我们所见,add方法执行分成了好几步,所有多个线程操作,可能会插队,比如:导致获取到同一个num=1000 值,而最后两个线程修改的结果为同一个num=1001,就会少增加1
volatile可以解决吗
-
答案是不可以,所以volatile有不保证原子性的特点。
- 现在我们在变量上加入关键字 volatile
现在我们在变量上加入关键字 volatile
package com.zxh.testValidate; public class Demo02 { // volatile 不保证原子性 private volatile static int num = 0; // 定义一个变量 public static void add(){ num++; // 对num进行+1操作 } public static void main(String[] args) { // 理论情况,num应该变成了20000 for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行 while(Thread.activeCount() > 2){ Thread.yield(); // 礼让其他线程先执行 } // 当其他线程执行完add()操作后,再输出结果 System.out.println("num:" + num); } }
并不能解决
首先我们通过反编译class文件知道了,add()方法被拆分成了好几步执行,虽然只有一句num++操作。
就因为被分成了几个步骤,所以当一个线程执行add()方法的时候,被打扰,也就不能保证原子性的操作,导致出修改的结果重复。
其他JUC已经提出了解决方法
查看官方文档
-
我们已经了解了concurrent和locks包下的大部分类和接口了。
-
只剩下atomic包,其实这个包就是JUC提供的保证变量原子性的包。
-
我们点进去看一下
-
其实是一些对基本类型封装的类,它们保证了原子性
代码:使用原子类看一下结果
package com.zxh.testValidate; import java.util.concurrent.atomic.AtomicInteger; public class Demo02 { // volatile 不保证原子性 // 使用原子类 保证 原子性 private volatile static AtomicInteger num = new AtomicInteger(); // 定义一个变量 public static void add(){ num.getAndIncrement(); // 对num进行+1操作 } public static void main(String[] args) { // 理论情况,num应该变成了20000 for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行 while(Thread.activeCount() > 2){ Thread.yield(); // 礼让其他线程先执行 } // 当其他线程执行完add()操作后,再输出结果 System.out.println("num:" + num); } }
成功解决!但是我们需要知道是怎么解决的,底层是怎么运行的?
千万不要以为,这个getAndIncrement()方法只是做了+1操作。
原子类的源码分析
2、发现通过unsafe这个变量进行了 + 1操作
3、查看这个变量,发现是一个Unsafe
类
4、点进去这个类,发现很多方法都是native的本地方法
所以总结:这些类的底层都直接和操作系统挂钩!直接在内存中修改值!Unsafe类是一个很特殊的存在!
3、指令重排
什么是指令重排?
你写的程序,计算机并不是按照你写的那样去执行的。
它会通过:源代码-->编译器优化的重排-->指令并行也重排-->内存系统也会重排-->执行
举个指令重排的栗子
1、指令重排不会影响结果
int x = 1; //1 int y = 2; //2 x = x + 5; //3 y = x * x; //4 我们所期望的运行顺序:1234 但是可能执行的时候经过指令重排变成 2134 1324的顺序 有没有可能顺序为:4123!不可能 因为处理器进行指令重排的时候,会考虑:数据之间的依赖,比如:第3条依赖第1条
2、指令重排会影响结果
假设 x y a b默认为0
线程B | |
---|---|
x=a | y=b |
b=1 |
正常的结果:x = 0,y = 0
但是经过指令重排,因为线程A和线程B里的指令各自没有依赖关系,所以可能变成:
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
指令重排导致诡异的结果:x = 2,y = 1
volatile为什么可以避免指令重排?
在内存中会有个内存屏障,阻挡CPU指令。作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
加上volatile,就是在上下加入了屏障
那个地方会使用volatie?
那就是下面要讲的单例模式,其中DCL懒汉式用到了
以上是关于14volatile(轻量级的同步机制)的主要内容,如果未能解决你的问题,请参考以下文章
轻量级的同步机制——volatile语义详解(可见性保证+禁止指令重排)