volatile的学习总结
1.volatile是Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM的同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
文章图片
JMM的三大特性
【volatile的学习总结】JMM是线程安全性获得的保证。因为JMM具有如下特点:
- 可见性:从主内存拷贝变量后,如果某一个线程在自己的工作内存中对变量进行了修改,然后写回了主内存,其它线程能第一时间看到,这就叫作可见性。
- 原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
- 有序性:禁止指令重排,按照规定的顺序去执行
3. 代码验证Volatile的可见性 代码示例:
/**
* Created by salmonzhang on 2020/7/4.
* 可见性代码实例
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = https://www.it610.com/article/new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in ...");
//暂停一会儿线程
try{ TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) { e.printStackTrace();
}
myData.addTo10();
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
},"Thread01").start();
while (myData.number == 0) {
//main线程一直在这里等待,直到number的值不再等于零
}
System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ...");
}
}
class MyData{
//int number = 0;
// 这里没有加volatile
volatile int number = 0;
// 这里加了volatile
public void addTo10() {
this.number = 10;
}
}
没有加volatile的运行结果:
文章图片
加了volatile的运行结果:
文章图片
总结:如果不加volatile关键字,则主线程会进入死循环,加了volatile时主线程运行正常,可以正常退出,说明加了volatile关键字后,当有一个线程修改了变量的值,其它线程会在第一时间知道,当前值作废,重新从主内存中获取值。这种修改变量的值,让其它线程第一时间知道,就叫作可见性。
4. 代码验证Volatile不保证原子性 代码示例:
/**
* Created by salmonzhang on 2020/7/4.
* 验证volatile不保证原子性
* 原子性是什么意思:
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
* 需要整体完整,要么同时成功,要么同时失败。保证数据的原子一致性
*/
public class VolatileDemo2 {
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for (int i = 1;
i <= 20;
i++){
new Thread(() -> {
for (int j = 0;
j < 1000;
j++) {
myData2.addPlusPuls();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部执行完成后,再用main线程取得最终的结果值看看是多少?
while (Thread.activeCount() > 2) { //后台默认有两个线程:GC线程和main线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "finally number value = "https://www.it610.com/article/+ myData2.number);
}
}
class MyData2{
volatile int number = 0;
// 这里加了volatile
public void addPlusPuls() {
number++;
}
}
运行结果:
文章图片
从代码的运行结果会发现:会出现number最终的结果有可能出现不是20000的时候,这就证明了volatile不能保证原子性。
5. volatile不能保证原子性的原因和解决方案
- 为什么volatile不能保证原子性?
由于多线程进程调度的关系,在某一时间段出现了丢失写值的情况。因为线程切换太快,会出现后面的线程会把前面的线程的值刚好覆盖。
例如:Thread1和Thread2同时从主内存中读取number的值1到自己的工作内存,并同时进行了+1的动作,当Thread1将2写会主内存的时候,由于线程的调度原因,Thread2并没有第一时间知道Thread1已经将number的值改为了2,而是直接将Thread1改的number值进行覆盖,这样就会导致数据丢失。
- 解决方案:
2.1. 直接在addPlusPuls前面加上synchronized
class MyData2{ volatile int number = 0; // 这里加了volatile public synchronized void addPlusPuls() { number++; } }
但是为了保证一个number++的原子性直接用synchronized,感觉有点重,类似于“杀鸡用牛刀”
2.2 用atomic
class MyData2{ AtomicInteger number = new AtomicInteger(); public void addPlusPuls() { number.getAndIncrement(); } }
- 计算机在执行程序时,为了提高性能,编译器的处理器通常会对指令做重排,一般有三种重排:
- 编译器的重排
- 指令并行的重排
- 内存系统的重排
- 编译器的重排
文章图片
- 单线程环境里确保程序最终执行的结果和代码执行的结果一致
- 处理器在进行重排序时,必须考虑指令之间的数据依懒性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果也是无法预测的
public void mySort(){
int x=11;
//语句1
int y=12;
//语句2
x=x+5;
//语句3
y=x*x;
//语句4
}
计算机执行的顺序可能是:
1234
2134
1324
问题:
请问语句4可以重排后变成第一条码?
存在数据的依赖性,所以没办法排到第一个
重排案例二:
文章图片
指令重排代码示例:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1;
// 这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题
flag = true;
}public void method02() {
if (flag) {
a = a + 3;
System.out.println("a = " + a);
}
}
}
这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题,例如指令重排后,method01中的flag=true先被Thread1执行了,此时Thread2又抢占到了线程资源去执行method02()时,此时的运行结果就是有问题的。运行结果就是a = 3,而不是正常情况下的a = 4
7. 单例模式下可能存在线程不安全 代码示例:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
};
//synchronized 解决单例的多线程问题,会显得比较重,整个方法都被锁住了,不建议这么写
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}public static void main(String[] args) {
//并发多线程后,会出现构造函数多次执行的情况
for (int i = 1;
i <= 10;
i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
运行结果:
文章图片
8. 单例模式下的volatile分析 1.代码示例:
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
//加上volatile,禁止编译器指令重排
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
};
/**
* DCL (double check Lock 双端检索机制)
*/
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}public static void main(String[] args) {
//并发多线程后,会出现构造函数多次执行的情况
for (int i = 1;
i <= 10;
i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
总结:
- 如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
- 原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。
- instance = new Singleton() 可以分为以下三步完成
memory = allocate(); // 1.分配对象空间 instance(memory); // 2.初始化对象 instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
- 步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。
- 发生重排
memory = allocate(); // 1.分配对象空间 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成 instance(memory); // 2.初始化对象
- 所以不加 volatile 返回的实例不为空,但可能是未初始化的实例
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量