首先简单介绍一下volatile的应用,volatile作为Java多线程中轻量级的同步措施,保证了多线程环境中“共享变量”的可见性。这里的可见性简单而言可以理解为当一个线程修改了一个共享变量的时候,另外的线程能够读到这个修改的值。下面就是volatile的具体定义和实现原理。上一篇Java内存模型

一、volatile的定义和实现原理

1、Java并发模型采用的方式

  a)线程通信的机制主要有两种:共享内存和消息传递。

  ①共享内存:线程之间共享程序的公共状态,通过写-读共享内存中的公共状态来进行隐式通信;

  ②消息传递:线程之间没有公共状态,线程之间 必须通过发送消息来显式通信。

  b)同步:用于控制不同线程之间操作发生相对顺序。在

  共享内存模型中,同步是显式的进行的,需要显示的指定某个方法或者代码块在线程执行期间互斥进行。

  消息传递模型中,由于消息的发送必定在消息的接受之前,所以同步是隐式的进行的。

  c)Java并发采用的是共享内存模型,线程之间通信总是隐式的进行,而且这个通信是对程序员透明的。那么我们需要了解的是这个隐式通信的底层工作机制。

2、volatile的定义

Java编程语言中允许线程访问共享变量,为了确保共享变量能够被准确和一致性的更新,线程应该确保通过排它锁单独获得这个变量。

3、volatile的底层实现原理

  a)在编写多线程程序中,使用volatile修饰的共享变量在进行写操作的时候,编译器生成的汇编代码中会多出一条lock指令,这条lock指令的作用:

①将当前处理器缓存行中的数据写回到系统内存
②这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

  b)参考下面的这张图理解

二、volatile的内存语义

1、volatile的特性

  a)首先我们来看对单个变量的读/写的实现(单个变量的情况可以看做是对同一个锁对这个变量的读/写进行了同步),看下面的例子

 package cn.jvm.test;

 public class TestVolatile1 {

     volatile long var1 = 0L;

     public void set(long l) {
// TODO Auto-generated method stub
var1 = l;
} public void getAndIncrement() {
// TODO Auto-generated method stub
var1 ++; //注意++操作
} public long get() {
return var1;
}
}

  上面的set和get操作在语义上和使用synchronized修饰后一样,即下面的这种写法

 package cn.jvm.test;

 public class TestVolatile1 {

     volatile long var1 = 0L;

     public synchronized void set(long l) {
// TODO Auto-generated method stub
var1 = l;
} public synchronized long get() {
return var1;
}
}

  b)但是在上面的用例中,我们使用的var1++操作,整体上没有原子性,所以如果使用多线程方粉getAndIncrement方法的话,会导致读出的数据和主存中不一致的情况。

  c)volatile变量的特性

①可见性:对一个volatile变量的读操作,总是能够看到对这个volatile变量最后的写入
②原子性:对任意单个volatile变量的读写具有原子性,但是对于volatile变量的复合型操作并不具备原子性

2、volatile写-读建立的happens-before关系

  a)看下面的代码实例

 package cn.jvm.test;

 public class TestVolatile2 {

     int a = 0;
volatile boolean flag = false; public void writer() {
a = 1;
flag = true;
} public void reader() {
if(flag) {
int i =a;
//...其他操作
}
}
}

  b)在上面的程序中,假设线程A执行write方法,线程B执行reader方法,根据happens-before规则有下面的关系:

程序次序规则:①happens-before②; ③happens-before④

volatile规则:②happens-before③

传递性规则:①happens-before④

  所以可以得到下面的这个状态图

3、volatile的写/读内存语义

  a)下面是volatile的写/读内存语义

①当写一个volatile变量时候,JMM会将线程对应的本地内存中的共享变量值刷新到主内存中
②当读一个volatile变量的时候,JMM会将线程对应的本地内存置为无效,然后从主内存中读取共享变量

  b)还是参照上面的程序示例,参考视图的模型来进行说明

  ①写内存语义的示意图:假设线程A执行writer方法,线程B执行reader方法,初始状况下线程A和B中的变量都是初始状态

  ②写内存语义的示意图:

三、volatile内存语义的实现

我们上面说到的基本上从宏观上而言都是说明了volatile保证内存可见性问题,volatile的另一个语义就是禁止指令重排序的优化。下面说一下volatile禁止指令重排序的实现细节

1、volatile重排序规则

①当第二个操作是volatile写的时候,不管第一个操作是什么,都不能进行指令重排序。这个规则确保volatile写之前的操作都不会被重排序到volatile写之后。
 也是为了保证volatile写对其他线程可见
②当第一个操作为volatile读的时候,不管第二个操作是什么,都不能进行重排序。确保volatile读之后的操作不会被重排序到volatile读之前
③当第一个操作是volatile写,第二个操作是volatile读的时候,不能进行重排序

  如下所示,上面的是下表中的总结。

2、内存屏障  

编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止对特定类型的处理器重排序。下面是集中策略,后面会说明这几种情况

①在每个volatile写操作之前插入StoreStore屏障
②在每个volatile写操作之后插入StoreLoad屏障
③在每个volatile读操作之后插入LoadLoad屏障
④在每个volatile读操作之后插入LoadStore屏障

3、内存屏障示例

  a)volatile写插入内存屏障之后的指令序列图

  b)volatile读插入内存屏障后的指令序列图

四、volatile与死循环问题

  1、先看下面的示例代码,观察运行结果,当共享变量isRunning 没有被声明为volatile的时候,main线程会在2秒之后将共享变量isRunning 置为false并且输出修改信息,这样新建的线程应该结束运行,但是实际上并没有,控制台中会一直保持运行的状态,并且不会打印线程结束执行;如下所示

 package cn.jvm.test;

 class ThreadDemo extends Thread {
private boolean isRunning = true;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行");
while(isRunning) { }
System.out.println(Thread.currentThread().getName() + " 结束执行");
}
public boolean isRunning() {
return isRunning;
}
public void SetIsRunning(boolean isRunning) {
this.isRunning = isRunning;
}
} public class TestVolatile4 {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
td.start();
try {
Thread.sleep(2000);
td.SetIsRunning(false);
System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}

  2、分析出现上面结果的原因

在启动线程ThreadDemo之后,变量isRunning被存在公共堆栈以及线程的私有堆栈中,后//续中线程一直在私有堆栈中取出isRunning的值,虽然main线程执行SetIsRunning方法修改了
isRunning的值,但是这个值并没有被Thread-//0线程所知,就像上面说的Thread-0取得值一直都是私有堆栈中的,所以不会知道isRunning被修改,也就不会退出循环

  3、按照上面的原因分析一下执行的时候的工作内存和主内存的情况,按照下面的分析我们很容易得出结论

上面的问题就是因为工作内存(私有堆栈)和主内存(公共堆栈)中的值不同步。
而按照我们上面说到的volatile使得单个变量保证线程可见性,就可以对程序修改保证共享变量在main线程中的修改对Thread-0线程可见(结合volatile的实现原理)

  4、修改之后的结果

 package cn.jvm.test;

 class ThreadDemo extends Thread {
private volatile boolean isRunning = true;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行");
while(isRunning) { }
System.out.println(Thread.currentThread().getName() + " 结束执行");
}
public boolean isRunning() {
return isRunning;
}
public void SetIsRunning(boolean isRunning) {
this.isRunning = isRunning;
}
} public class TestVolatile4 {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
td.start();
try {
Thread.sleep(2000);
td.SetIsRunning(false);
System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}

将isRunning修改为volatile

五、volatile对于复合操作非原子性问题

  1、volatile能保证对单个变量在多线程之间的可见性问题,但是对于单个变量的复合操作不能保证原子性,如下代码示例,运行结果为,当然这个结果是随机的,但是不能保证运行结果是100000

在没有使用同步操作之前,虽然count变量是volatile的,但是由于count++操作是个复合操作
①从内存中取出count的值
②计算count的值
③将count的值写到内存中
这个复合操作由于volatile不能保证原子性,所以就会出现错误
 package cn.jvm.test;

 import java.util.ArrayList;
import java.util.List; public class TestVolatile5 {
volatile int count = 0;
/*synchronized*/ void m(){
for(int i = 0; i < 10000; i++){
count++;
}
} public static void main(String[] args) {
final TestVolatile5 t = new TestVolatile5();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++){
threads.add(new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}));
}
for(Thread thread : threads){
thread.start();
}
for(Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(t.count);
}
}

  2、下面按照JVM的内存工作来分析一下,即当前一个线程在计算count变量的时候,另一个线程已经修改了count变量的值,这样就必然会出现错误。所以对于这种复合操作就需要使用原子类或者使用synchronized来保证原子性(保证同步)

  3、修改后的synchronized和使用原子类如下所示

 package cn.jvm.test;

 import java.util.ArrayList;
import java.util.List; public class TestVolatile5 {
int count = 0;
synchronized void m(){
for(int i = 0; i < 10000; i++){
count++;
}
} public static void main(String[] args) {
final TestVolatile5 t = new TestVolatile5();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++){
threads.add(new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}));
}
for(Thread thread : threads){
thread.start();
}
for(Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(t.count);
}
}

使用synchronized

 package cn.jvm.test;

 import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; public class TestVolatile5 {
AtomicInteger count = new AtomicInteger(0);
void m(){
for(int i = 0; i < 10000; i++){
count.getAndIncrement();
}
} public static void main(String[] args) {
final TestVolatile5 t = new TestVolatile5();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++){
threads.add(new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}));
}
for(Thread thread : threads){
thread.start();
}
for(Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(t.count);
}
}

使用原子类型

参考自《Java并发编程的艺术》 《Java多线程编程核心技术》

Java并发编程基础之volatile的更多相关文章

  1. Java并发编程基础

    Java并发编程基础 1. 并发 1.1. 什么是并发? 并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互 ...

  2. 并发-Java并发编程基础

    Java并发编程基础 并发 在计算机科学中,并发是指将一个程序,算法划分为若干个逻辑组成部分,这些部分可以以任何顺序进行执行,但与最终顺序执行的结果一致.并发可以在多核操作系统上显著的提高程序运行速度 ...

  3. Java并发编程--基础进阶高级(完结)

    Java并发编程--基础进阶高级完整笔记. 这都不知道是第几次刷狂神的JUC并发编程了,从第一次的迷茫到现在比较清晰,算是个大进步了,之前JUC笔记不见了,重新做一套笔记. 参考链接:https:// ...

  4. Java并发编程之三:volatile关键字解析 转载

    目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用>   volatile这个关键字可能很多朋友都听说过,或许也都用过 ...

  5. Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  6. Java并发编程基础-线程安全问题及JMM(volatile)

    什么情况下应该使用多线程 : 线程出现的目的是什么?解决进程中多任务的实时性问题?其实简单来说,也就是解决“阻塞”的问题,阻塞的意思就是程序运行到某个函数或过程后等待某些事件发生而暂时停止 CPU 占 ...

  7. Java并发编程知识点总结Volatile、Synchronized、Lock实现原理

    Volatile关键字及其实现原理 在多线程并发编程中,Volatile可以理解为轻量级的Synchronized,用volatile关键字声明的变量,叫做共享变量,其保证了变量的“可见性”以及“有序 ...

  8. 多线程(一)java并发编程基础知识

    线程的应用 如何应用多线程 在 Java 中,有多种方式来实现多线程.继承 Thread 类.实现 Runnable 接口.使用 ExecutorService.Callable.Future 实现带 ...

  9. Java并发编程学习:volatile关键字解析

    转载:https://www.cnblogs.com/dolphin0520/p/3920373.html 写的非常棒,好东西要分享一下 Java并发编程:volatile关键字解析 volatile ...

随机推荐

  1. js 对象,数组,字符串,相互转换

    1:对象转换数组 let obj = {'val1':1, 'val2':2, 'val3':3, 'val4':4}; var arr = [] for (let i in obj) { //取键 ...

  2. The Apache Tomcat installation at this directory is version 8.5.40. A Tomcat 8.0 installation is expected.

    问题描述 Eclipse 配置 Apache Tomcat 8.5.40(8.0.x 以上版本),会报如下错误信息: 解决方法 1)在 Apache Tomcat 的安装目录中找到 lib 目录下的 ...

  3. 基于Vue2.x的小米商城移动端项目

    初学vue已经有一段时间,为了检验自己的学习成果,决定做一个项目作为一个阶段性总结,项目花了差不多半个月时间,目前实现了7个页面,商城的主要功能基本实现,代码已经放到github上面. 这个项目把大部 ...

  4. 分割字符串和截取字符串:split 和substring

    //按“,”截取字符串 String id="123123,234534,453456"; String[] idArry = id.trim().split(",&qu ...

  5. auth组件

    Django auth认证组件 简介 ''' Django auth认证组件提供了用户表的构建方式,认证接口,会话登录与注销接口. 中间件将会话登录用户保存到request对象中,这样不用从会话中获取 ...

  6. 微信小程序支付遇到的坑

    1,微信公众号支付和微信小程序支付有差异 微信公众号:可以直接跳转走h5的微信支付 微信小程序:在测试环境.沙箱环境使用微信公众号的跳转支付没有问题,在线上存在支付异常 最后商讨的解决方法 openi ...

  7. leetcode刷题六<z字形变换>

    将一个给定字符串根据给定的行数,以从上往下.从左到右进行 Z 字形排列. 比如输入字符串为 时,排列如下: L C I R E T O E S I I G E D H N 之后,你的输出需要从左往右逐 ...

  8. SQL功能分类

    DDL  数据定义语言:创建表 ,库,列 DML 数据操作语言:用来操作数据库中的记录 DQL 数据查询语言 :用来查询数据 DCL 数据控制语言:定义访问权限和安全级别 —————————————— ...

  9. Javascript 标识符及同名标识符的优先级

    一.定义 标识符(Identifier)就是一个名字,用来对变量.函数.属性.参数进行命名,或者用做某些循环语句中的跳转位置的标记. //变量 var Identifier = 123; //属性 ( ...

  10. LPC 网络编程

    LPC有五种不同的通信模式(socket模式) ① MUD (面向连接的通信模式) 可以把除Object以外的所有LPC模型从一个MUD传到另一个MUD 弊端: 无法传送物件造成了穿越MUD的功能(即 ...