概念

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. 前端工程化(二)---webpack配置

    导航 前端工程化(一)---工程基础目录搭建 前端工程化(二)---webpack配置 前端工程化(三)---Vue的开发模式 前端工程化(四)---helloWord 继续上一遍的配置,本节主要记录 ...

  2. Netty 笔记

    1.Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端. 2.早期Java API 使用的阻塞函数 // 创建一个新的ServerSocket, ...

  3. Android开发学习总结(三)——appcompat_v7项目说明

    一.appcompat_v7项目说明 今天来说一下appcompat_v7项目的问题,使用eclipse创建Android项目时,发现project列表中会多创建出一个appcompat_v7项目,这 ...

  4. SVG的动态之美-搜狗地铁图重构散记

    搜狗地图发布了新版的移动端地铁图,改版初衷是为了用户交互体验的提升以及性能的改善.原版地铁图被用户吐槽最多的是pinch缩放不流畅.无过渡动画.拖拽边界不合理等等,大体上都是交互体验上的问题.实际上原 ...

  5. Linux时间子系统之二:Alarm Timer

    一.前言 严格来讲Alarm Timer也算POSIX Timer一部分,包含两种类型CLOCK_REALTIME_ALARM和CLOCK_BOOTTIME_ALARM.分别是在CLOCK_REALT ...

  6. Can I use MyBatis to generate Dynamic SQL without executing it?

    Although MyBatis was designed to execute the query after it builds it, you can make use of it's conf ...

  7. 你不知道的JavaScript--Item1 严格模式

    本文转自[阮一峰博客]:http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html 一.概述 除了正常运行模式,ECMAscr ...

  8. Windows下Pytesser安装

    安装流程   详细步骤 安装PIL 32位机器:去到PIL官网进行安装 64位机器:建议使用Pillow代替PIL,PIL官网的安装包在64位机器下无法找到,安装Pillow也非常简单,使用pip就可 ...

  9. Python字符编码与转码

    字符编码与转码 注: 该图转自 http://www.cnblogs.com/luotianshuai/p/5735051.html. Python2 解码,编码 #Python2 #-*- codi ...

  10. engine.go

    package ;;;;;;;;) ;;;;;) ) ) ) ;; ;;, types.DocumentIndexData{}, true)     for {         runtime.Gos ...