概念

JMM规范解决了线程安全的问题,主要三个方面:原子性、可见性、有序性,借助于synchronized关键字体现,可以有效地保障线程安全(前提是你正确运用)
之前说过,这三个特性并不一定需要全部同时达到,在有些场景,部分达成也能够做到线程安全。
volatile就是这样一个存在,对可见性和有序性进行保障

可见性

volatile字面意思,易变的,不稳定的,在Java中含义也是如此
想要保证可见性,就要保障一个线程对于数据的操作,能够及时的对其他线程可见
volatile会通知底层,指示这个变量读取时,不要通过本地缓存,而是直接去主存中读取(或者说本地内存失效,必须去主存读取),这样如果一个线程对于数据完成写入到主存,另外线程进行读取时,就可以第一时间读取到新值,而非旧值,所以所谓不稳定,就是指可能会被其他线程同时并发修改,所以你要去主存中去重新读取。
他会让写线程冲刷写缓存,读线程刷新读缓存,简言之就是操作后立刻会刷新数据,读取前也会刷新数据;
以保证最新值可以及时更新到主存以及读线程及时的读取到最新值。
注意:
如果Reader对于这个共享变量x的读取操作有很多个步骤,比如x=1;y=x;y=y+1;y=y+2;等等 最后x=y;,如果没有原子性保障,很显然,如果已经执行过了y=x;再往后的操作过程中,如果x的值再次被改变了,此时Reader中的y是无法改变的,这就出现问题了
所以此处的可见性要注意区分,在某些场景想要线程安全的话,可见性对原子性是有依赖的
可见性指的是在你需要的时刻,如果被别人修改了,重新读取新的,但是如果你用过了,单纯的可见性并不能保证后续没问题。

有序性

volatile关键字将会直接禁止JVM和处理器对关键字修饰的指令重排序,但是对于volatile关键字修饰的前后的、无依赖的指令,可以进行重排序
被volatile修饰的变量,可以认为插入了一个内存屏障,他会进行如下保障:
  • 确保指令重排序时不会将其后面的代码排到内存屏障之前
  • 确保指令重排序时不会将其前面的代码排到内存屏障之后
  • 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
  • 强制将线程工作内存中值的修改刷新至主内存中
  • 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效
比如
int x = 0;
int y = 1;
volatile int z=20;
x++;
y--;
在语句volatile int z=20之前,先执行x的定义还是先执行y的定义,我们并不关心,只要能够百分之百地保证在执行到z=20的时候x=0, y=1,同理关于x的自增以及y的自减操作都必须在z=20以后才能发生。这个结果就是上面的逻辑处理后的结果。
 
综上所述,volatile可以对可见性以及有序性进行保障。
那么volatile的原子性如何?

原子性

如下面示例,共享变量count是volatile的,在add方法中,对他进行自增,运行几次后分别查看结果
package test1;
public class T12 {
public static volatile int count = 0;
public static void add() {
count++;
}
public static void main(String[] args) {
//创建10个线程,每个线程循环1000次,最终结果应该是10,000
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 确认其他线程都结束了,否则不继续执行(确认当前线程组以及子线程组活动线程的个数,JDK8中这个值设置为2),后续有更好的方法完成等待
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("count: " + count);
}
}

 

10个线程,每个线程1000次循环,按理来说最终的结果应该是1000
从结果可以看得出来,并不是线程安全的,但是既然volatile保障了可见性与有序性,可以推断出来并没有做到原子性
问题出在哪里?
关键在于count++;自增操作,并不是直接的赋值操作,比如x=1;
他可以简单的理解为三个步骤:
  1. 读取count的值;
  2. 操作count的值;
  3. 回写count的值;
volatile可以保障在第一步的时候,读取到了正确的值,但是由于不是原子的,在接下来的操作过程中,count的值,可能已经被更新过了,也就是读取到了旧值
继续使用这个旧值很显然就把别人的更新抹掉了,你读取的1,可能此时应该是2了,但是你操作后还是2,无故的擦除了别人的增加,所以结果才会出现小于10000的情况
因为是自增操作,所以使用旧值会导致小于10000
如果把初始值设置为10000,使用自减count--,使用旧值就可能会导致别人的减量被擦除了,最终大于0,不妨修改为自减运算试一下
从结果看得出来,我们的推断没错,就是使用了旧值
这就是前面说到的线程安全,单纯的依赖可见性是不能保障的,还需要依赖原子性
因为在第一步的时候,尽管获取到的值肯定是最新的,但是接下来的过程中呢?
值仍旧可能被改变,因为并不是原子的
比如,装着饮料的瓶子,你从其中取饮料
可见性可以保障你要倒饮料的时候,瓶子里面是可乐你到出来的是可乐,装的是雪碧,倒出来就是雪碧,但是如果你把可乐倒进自己的杯子里面了,瓶子瞬间换成雪碧,你杯子里面的可乐会变化吗?
 
回想下之前设计模式中介绍过的单例模式,有一种实现方式是双重检查法
public class LazySingleton {
private LazySingleton() {
} private static volatile LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
注意:

private static volatile  LazySingleton singleton = null;

使用volatile修饰
因为实例创建语句:singleton = new LazySingleton(); ,就不是一个原子操作 
他可能需要下面三个步骤
  • 分配对象需要的内存空间
  • 将singleton指向分配的内存空间
  • 调用构造函数来初始化对象
计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整
也就是上面三个步骤的顺序是不能够保证唯一的
如果先分配对象需要的内存,然后将singleton指向分配的内存空间,最后调用构造方法初始化的话
 
假如当singleton指向分配的内存空间后,此时被另外线程抢占(由于不是原子操作所以可能被中间抢占)
线程2此时执行到第一个if (singleton == null)
此时不为空,那么不需要等待线程1结束,直接返回singleton了
显然,此时的singleton都还没有完全初始化,就被拿出去使用了
根本问题就在于写操作未结束,就进行了读操作
重排序导致了线程的安全问题
此时可以给 singleton 的声明加上volatile关键字,以保障有序性
 
上面的两个示例,看起来都是没有保障原子性,但是为什么一个使用volatile修饰就可以,而另外一个则不行?
对于count++,运算结果的正确性依赖count当前的值本身,而且可能存在多个线程对他进行修改,而singleton则不依赖,而且也不会多个线程进行修改
所以说,volatile的使用要看具体的场景,这也是为什么被称之为轻量级的synchronized的原因,他不能从原子性、可见性、有序性三个角度进行保障。
所以从上面这些点也可以看得出来,volatile并不能替代synchronized,很关键的一个点就是他并不能保障原子性

volatile与synchronized对比

总结

volatile是一种轻量级的同步方式(轻量级的synchronized,也就是阉割版的synchronized)
抛开性能的角度看,synchronized的正确使用可以百分百解决同步问题,但是volatile却并不能完全解决同步问题,因为他缺乏一个很重要的保障---原子性
原子性能够保障不可分割,一旦不能对原子性进行保障,一旦一个变量的修改依赖自身,比如i++,也就是i=i+1;依赖自身的值,一旦再多线程环境中,仍旧可能会出错
所以如果换一个思路理解的话,可以这样:
对于线程安全问题,主要是三个方面,原子性、可见性、有序性,不过并不一定所有的场景都需要三者完全保障;
对于synchronized关键字都进行了保障,可以用于线程安全的同步问题
对于volatile,他对可见性和有序性进行了保障,所以如果在有些场景下,如果仅仅保障了这两者就可以达到线程安全,那么volatile也可以用于线程的同步
所以说synchronized可以用于同步,volatile可以用于部分场景的线程同步
刚才提到对于i++,仅仅借助于volatile,他相当于i=i+1,依赖自身的值的内容,所以多线程会出问题,如果只有一个线程才会执行这个操作就不会出现问题
另外,如果对于一个操作,比如i=j+1;j也是一个共享变量,很显然多线程场景下,仍旧可能出现问题
所以如果你使用volatile保障线程安全,需要非常慎重,必要的时候,仍旧需要借助于synchronized关键字进行同步,进一步对原子性进行保障。

java 轻量级同步volatile关键字简介与可见性有序性与synchronized区别 多线程中篇(十二)的更多相关文章

  1. sleep、yield、join方法简介与用法 sleep与wait区别 多线程中篇(十五)

    Object中的wait.notify.notifyAll,可以用于线程间的通信,核心原理为借助于监视器的入口集与等待集逻辑 通过这三个方法完成线程在指定锁(监视器)上的等待与唤醒,这三个方法是以锁( ...

  2. Java 并发:volatile 关键字解析

    摘要: 在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保 ...

  3. java并发系列(六)-----Java并发:volatile关键字解析

    在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...

  4. 一起来看看java并发中volatile关键字的神奇之处

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volati ...

  5. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  6. volatile关键字与内存可见性

    前言 首先,我们使用多线程的目的在于提高程序的效率,但是如果使用不当,不仅不能提高效率,反而会使程序的性能更低,因为多线程涉及到线程之间的调度.CPU上下文的切换以及包括线程的创建.销毁和同步等等,开 ...

  7. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  8. 【JUC系列第一篇】-Volatile关键字及内存可见性

    作者:毕来生 微信:878799579 什么是JUC? JUC全称 java.util.concurrent 是在并发编程中很常用的实用工具类 2.Volatile关键字 1.如果一个变量被volat ...

  9. java并发:volatile关键字

    java并发需要保证原子性,可见性,有序性. http://www.cnblogs.com/expiator/p/9226775.html 一.volatile关键字作用如下: 1.volatile关 ...

随机推荐

  1. [CVPR 2016] Weakly Supervised Deep Detection Networks论文笔记

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px "Helvetica Neue"; color: #323333 } p. ...

  2. 解决redis connection refused: connect无法连接redis

    环境 VM VirtualBox安装虚拟机CentOS 7 1.Redis.conf配置文件中 注释掉 bind 127.0.0.1 2.防火墙关闭(或添加可访问的端口,具体不在此描述) 最后一个坑 ...

  3. java基础 lang包 详细介绍

    Java.javax和org.其中以java开头的包名是JDK的基础语言包,以javax开头的属 (org是organization的简写).而在JDK API中还包含了一些以com.sun开头的包名 ...

  4. Redis 5种主要数据类型和命令

    redis是键值对的数据库,有5中主要数据类型: 字符串类型(string),散列类型(hash),列表类型(list),集合类型(set),有序集合类型(zset) 几个基本的命令: KEYS * ...

  5. SSM-MyBatis-18:Mybatis中二级缓存和第三方Ehcache配置

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 二级缓存 Mybatis中,默认二级缓存是开启的.可以关闭. 一级缓存开启的.可以被卸载吗?不可以的.一级缓存 ...

  6. Linux kernel的中断子系统之(二):IRQ Domain介绍

    返回目录:<ARM-Linux中断系统>. 总结:一.二概述了软硬件不同角度的IRQ Number和HW Interrupt ID,这就需要他们之间架个桥梁. 三介绍了架设这种桥梁的几种方 ...

  7. Web前端原生JavaScript浅谈轮播图

    1.一直来说轮播图都是困扰刚进业内小白的一大难点,因为我们不仅需要自己作出一个比较完美的运动框架(虽然网上一抓一大把,但是哪有比自己做出来实现的有成就感,不是吗?^_^),还必须需要非常关键性的把握住 ...

  8. 1.Spring Framework 5.0 入门篇

    1.为什么学习Spring? 随着对Java EE的不断接触和理解,你会发现Spring  在各个企业和项目中发挥着越来越重要的作用.掌握Spring 已成为我们IT行业生存必学的本领之一. Spri ...

  9. 系列博文-Three.js入门指南(张雯莉)-网格 setInterval方法 requestAnimationFrame方法 使用stat.js记录FPS

    第6章 动画 在本章之前,所有画面都是静止的,本章将介绍如果使用Three.js进行动态画面的渲染.此外,将会介绍一个Three.js作者写的另外一个库,用来观测每秒帧数(FPS). CSS3动画那么 ...

  10. 如何将网页保存为PDF文件

    怎样将网页保存为PDF文件... 问题: 很多时候我们需要将网页上的内容,在排版不变的情况下完整的保存下来,那么用pdf格式是最好的效果了,还图文并茂,效果与真实的网页很相似,如果另存为网页的话,会下 ...