1.volatile是Java虚拟机提供的轻量级的同步机制
  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

2. Java内存模型(JMM)

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定义了程序中的各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM的同步规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存

  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存

  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存时每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要的访问过程如下图:

JMM的三大特性

JMM是线程安全性获得的保证。因为JMM具有如下特点:

  1. 可见性:从主内存拷贝变量后,如果某一个线程在自己的工作内存中对变量进行了修改,然后写回了主内存,其它线程能第一时间看到,这就叫作可见性。

  2. 原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割

  3. 有序性:禁止指令重排,按照规定的顺序去执行

综上所述,volatile满足JMM三大特性中的两个,即可见性和有序性,volatile并不满足原子性,所以说volatile是轻量级的同步机制。

3. 代码验证Volatile的可见性

代码示例:

 /*** Created by salmonzhang on 2020/7/4.* 可见性代码实例*/public class VolatileDemo {public static void main(String[] args) {MyData myData = 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; // 这里没有加volatilevolatile int number = 0; // 这里加了volatilepublic 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 = " + myData2.number);}}class MyData2{volatile int number = 0; // 这里加了volatilepublic void addPlusPuls() {number++;}}

运行结果:

从代码的运行结果会发现:会出现number最终的结果有可能出现不是20000的时候,这就证明了volatile不能保证原子性。

5. volatile不能保证原子性的原因和解决方案
  1. 为什么volatile不能保证原子性?

    由于多线程进程调度的关系,在某一时间段出现了丢失写值的情况。因为线程切换太快,会出现后面的线程会把前面的线程的值刚好覆盖。

    例如:Thread1和Thread2同时从主内存中读取number的值1到自己的工作内存,并同时进行了+1的动作,当Thread1将2写会主内存的时候,由于线程的调度原因,Thread2并没有第一时间知道Thread1已经将number的值改为了2,而是直接将Thread1改的number值进行覆盖,这样就会导致数据丢失。

  2. 解决方案:

    2.1. 直接在addPlusPuls前面加上synchronized

    class MyData2{volatile int number = 0; // 这里加了volatilepublic synchronized void addPlusPuls() {number++;}
    }
    

    但是为了保证一个number++的原子性直接用synchronized,感觉有点重,类似于“杀鸡用牛刀”

    2.2 用atomic

    class MyData2{AtomicInteger number = new AtomicInteger();public void addPlusPuls() {number.getAndIncrement();}
    }
    
7. 有序性
  1. 计算机在执行程序时,为了提高性能,编译器的处理器通常会对指令做重排,一般有三种重排:

    • 编译器的重排

    • 指令并行的重排

    • 内存系统的重排

  1. 单线程环境里确保程序最终执行的结果和代码执行的结果一致

  2. 处理器在进行重排序时,必须考虑指令之间的数据依懒性

  3. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果也是无法预测的

重排案例一:

public void mySort(){int x=11;//语句1int y=12;//语句2x=x+5;//语句3y=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 返回的实例不为空,但可能是未初始化的实例

非常感谢您的耐心阅读,希望我的文章对您有帮助。欢迎点评、转发或分享给您的朋友或技术群。

volatile的学习总结相关推荐

  1. Android Volatile 关键字学习

    面试官:你平时是怎么创建单例的? 我:我一般用DCL双重检锁的方式来创建单例,然后为 instance 加上 volatile 修饰,防止 DCL 失效. 面试官:那你可以具体说说 volatile ...

  2. java中volatile关键字---学习笔记

    volatile关键字的作用 在java内存模型中,线程之间共享堆内存(对应主内存),但又各自拥有自己的本地内存--栈内存,线程的栈内存中缓存有共享变量的副本,但如果是被volatile修饰的变量,线 ...

  3. Java中的Volatile如何工作? Java中的volatile关键字示例

    如何在Java中使用Volatile关键字 在Java采访中,什么是volatile变量以及何时在Java中使用volatile变量是Java 采访中一个著名的多线程采访问题 . 尽管许多程序员都知道 ...

  4. c语言中用两个n表示什么格式,C语言中‘\n'为什么能表示CRLF两个字节

    为什么要说这个简单的问题? 众所周知,在Windows下文本文件的换行符是CRLF,占两个字节.在Unix下是LF,占一个字节.(还有奇葩的Mac是CR).但是C语言中直接printf一个 '\n', ...

  5. c语言atan的作用,C语言中的atan和atan2

    在C语言的math.h或C++中的cmath中有两个求反正切的函数atan(double x)与atan2(double y,double x)  他们返回的值是弧度 要转化为角度再自己处理下. 前者 ...

  6. c语言其他函数调用main函数,C语言中的main函数以及main函数是如何被调用的

    main函数是C语言中比较特殊的函数,C程序总是从main函数开始执行,main函数的原型是: int main(int argc, char *argv[]); 其中argc是命令行参数的个数,ar ...

  7. c语言中short作用,C语言中short的意思

    short和int等一样,是C或C++的一种内部数据类型.用于表示有符号整数. 不同的是,他们在内存中所占的空间大小不同,short通常为int所占一半,也有一些实现为和int一样,但不会比int大. ...

  8. c语言putchar与getchar,C语言中的getchar和putchar详解

    首先给出<The_C_Programming_Language>这本书中的例子: #include int main() { int c; c = getchar(); while (c  ...

  9. 一个具体的例子学习Java volatile关键字

    相信大多数Java程序员都学习过volatile这个关键字的用法.百度百科上对volatile的定义: volatile是一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和 ...

最新文章

  1. 硅谷人眼中的2018年十大前沿科技预测
  2. python 创建目录_Python虚拟环境的搭建与使用
  3. bigdecimal 等于0_好程序员Java培训分享BigDecimal的用法
  4. 单源最短路径(Dijkstra算法)
  5. 超18万人次下载使用的 Cloud Toolkit 的成长历程
  6. plsql数据库异常---plsql 登录后,提示数据库字符集(AL32UTF8)和客户端字符集(ZHS16GBK)不一致 .
  7. 抖音视频怎么去掉抖音的水印?
  8. VSFTPD 上传文件 200 227 553错误
  9. Matlab绘制跳动的心
  10. 技术团队绩效考核怎么搞?详解OKR、MBO、KPI、BSC
  11. 配置表单和报表以使用HTTP Server(OHS)
  12. mc是用java写的吗_都说MC的代码特别差劲,你觉得它在所有游戏中,能排第几?...
  13. 8b10b编码源码 matlab,8b/10b编码技术系列(一):Serdes、CDR、K码
  14. 视频图像处理技术优势安防视频监控应用
  15. 华为云区块链三大核心技术国际标准立项通过
  16. 微型计算机的特点及其主板构成,第1章 计算机基础知识教案
  17. 【前端】水平垂直居中的几种方式
  18. 赛微微电启动上市发行:生产依赖外协加工厂商,市盈率偏高
  19. tec控制pid程序_利用 SPICE 设计 TEC 温度环路 PID 控制
  20. python教程txt免费下载-《Python编程 从入门到实践》高清电子书免费下载

热门文章

  1. LeetCode_脑筋急转弯
  2. 奶茶妹妹亏本甩卖悉尼豪宅 当年与刘强东澳洲办盛大婚礼
  3. python数据分析与应用
  4. 【转】C++调用Matlab的.m文件
  5. thinkphp phpexcel导出
  6. 【技术干货】浏览器工作原理和常见WEB攻击 (下)
  7. [Android学习笔记二] View转化Bitmap
  8. Struts ActionForm简单理解
  9. javaSE基础之记事本编程
  10. css3中transform的用法