Java并发编程之深入理解volatile

个人博客请访问 http://www.x0100.top       

1. 保证可见性

volatile保证了不同线程对volatile修饰变量进行操作时的可见性。

对一个volatile变量的读,(任意线程)总是能看到对这个volatile变量最后的写入。

  1. 一个线程修改volatile变量的值时,该变量的新值会立即刷新到主内存中,这个新值对其他线程来说是立即可见的。

  2. 一个线程读取volatile变量的值时,该变量在本地内存中缓存无效,需要到主内存中读取。

举例:

中断线程时常采用这种标记办法。

boolean stop = false;// 是否中断线程1标志
//Tread1
new Thread() {
    public void run() {
        while(!stop) {
          doSomething();
        }
    };
}.start();
//Tread2
new Thread() {
    public void run() {
        stop = true;
    };
}.start();

目的: Tread2设置stop=true时,Tread1读取到stop=true,Tread1中断执行。

问题: 虽然大多数时候可以达到中断线程1的目的,但是有可能发生Tread2设置stop=true后,Thread1未被中断的情况,而且这种情况引发的都是比较严重的线上问题,排查难度很大。

问题分析: Tread2设置stop=true时,并未将stop=true刷到主内存,导致Tread1到主内存中读取到的仍然是stop=false,Tread1就会继续执行。也就是有内存可见性问题。

解决: stop变量用volatile修饰。
Tread2设置stop=true时,立即将volatile修饰的变量stop=true刷到主内存;
Tread1读取stop的值时,会到主内存中读取最新的stop值。

2. 保证有序性

volatile关键字能禁止指令重排序,保证了程序会严格按照代码的先后顺序执行,即保证了有序性。

volatile的禁止重排序规则:

1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

举例:

boolean inited = false;// 初始化完成标志
//线程1:初始化完成,设置inited=true
new Thread() {
    public void run() {
        context = loadContext();   //语句1
        inited = true;             //语句2
    };
}.start();
//线程2:每隔1s检查是否完成初始化,初始化完成之后执行doSomething方法
new Thread() {
    public void run() {
        while(!inited){
          Thread.sleep(1000);
        }
        doSomething(context);
    };
}.start();

目的: 线程1初始化配置,初始化完成,设置inited=true。线程2每隔1s检查是否完成初始化,初始化完成之后执行doSomething方法。

问题: 线程1中,语句1和语句2之间不存在数据依赖关系,JMM允许这种重排序。如果在程序执行过程中发生重排序,先执行语句2后执行语句1,会发生什么情况?

当线程1先执行语句2时,配置并未加载,而inited=true设置初始化完成了。线程2执行时,读取到inited=true,直接执行doSomething方法,而此时配置未加载,程序执行就会有问题。

解决: volatile修饰inited变量。
volatile修饰inited,“当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。”,保证线程1中语句1与语句2不能重排序。

3. 不保证原子性

volatile是不能保证原子性的。

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

举例:

public class VolatileTest {
    public volatile int a = 0;

    public void increase() {
        a++;
    }

    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

        while (Thread.activeCount() > 1) {
            // 保证前面的线程都执行完
            Thread.yield();
        }
        System.out.println(test.a);
    }
}

 

目的: 10个线程将inc加到10000。

结果: 每次运行,得到的结果都小于10000。

原因分析:

首先来看a++操作,其实包括三个操作:
  ①读取a=0;
  ②计算0+1=1;
  ③将1赋值给a;
保证a++的原子性,就是保证这三个操作在一个线程没有执行完之前,不能被其他线程执行。

一个可能的执行时序图如下:

关键一步:线程2在读取a的值时,线程1还没有完成a=1的赋值操作,导致线程2读取到当前a=0,所以线程2的计算结果也是a=1。

问题在于没有保证a++操作的原子性。如果保证a++的原子性,线程1在执行完三个操作之前,线程2不能执行a++,那么就可以保证在线程2执行a++时,读取到a=1,从而得到正确的结果。

解决:

  1. synchronized保证原子性,用synchronized修饰increase()方法。

  2. CAS来实现原子性操作,AtomicInteger修饰变量a。

4. volatile实现原理

volatile保证有序性原理

前文介绍过,JMM通过插入内存屏障指令来禁止特定类型的重排序。

java编译器在生成字节码时,在volatile变量操作前后的指令序列中插入内存屏障来禁止特定类型的重排序。

volatile内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

内存屏障

Store:数据对其他处理器可见(即:刷新到内存中)
Load:让缓存中的数据失效,重新从主内存加载数据

volatile保证可见性原理

volatile内存屏障插入策略中有一条,“在每个volatile写操作的后面插入一个StoreLoad屏障”。

StoreLoad屏障会生成一个Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事:

1. 将当前处理器缓存行的数据写回到系统内存。
2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

volatile内存可见的写-读过程:

  1. volatile修饰的变量进行写操作。

  2. 由于编译期间JMM插入一个StoreLoad内存屏障,JVM就会向处理器发送一条Lock前缀的指令。

  3. Lock前缀的指令将该变量所在缓存行的数据写回到主内存中,并使其他处理器中缓存了该变量内存地址的数据失效。

  4. 当其他线程读取volatile修饰的变量时,本地内存中的缓存失效,就会到到主内存中读取最新的数据。

总结

并发编程中,常用volatile修饰变量以保证变量的修改对其他线程可见。

volatile可以保证可见性和有序性,不能保证原子性。

volatile是通过插入内存屏障禁止重排序来保证可见性和有序性的。

热门文章

暂无图片
编程学习 ·

处理服务器cpu 占用高达99%问题

102服务器cpu 占用高达99%,查看服务百度了下这个服务是 swap分区的作用是当物理内存不足时,会将一部分硬盘当做虚拟内存来使用。 kswapd0 占用过高是因为 物理内存不足,使用swap分区与内存换页操作交换数据,导致CPU占用过高。 所以将服务器升级了内存和cpu 然而 服务器扩容…
暂无图片
编程学习 ·

solr自动更新索引,tomcat+solr

核心文件夹: tomcat-8.0.35-search------端口8888 solr-7.2.0------端口8984 核心配置: 用于配置solr索引的定时增量更新和全部更新,两个文件保持一致就可以。 /tomcat/tomcat-8.0.35-search/bin/solr/conf/dataimport.properties /solr-7.2.0/server/solr/chuai/conf/datai…
暂无图片
编程学习 ·

智能电视良心分享:沉浸式音画享受旗舰级品质做工

上周,我家工作了五年的电视结束了它的“职业生涯”。面对市面上林林总总的智能电视品牌,我在纠结良久之后,最终将目光锁定在荣耀智慧屏X1身上,趁着618打折,入手了一台。不得不说这款电视最近挺火,但是性能咋样,还得亲身体验过才有发言权。买电视我最关心还是音画质。很多…
暂无图片
编程学习 ·

Centos7实现MySQL数据库备份与恢复

简介MySQL数据库的备份可以分为逻辑备份和物理备份,逻辑备份工具主要为:mysqldump而物理备份工具主要为:xtrabackup,两种备份方式各有优缺点备份工具mysqldumpxtrabackup优点支持热备份和增量备份,需要磁盘空间小支持热备份和增量备份,业务影响小,停机时间短,缺点业务影…
暂无图片
编程学习 ·

2020李宏毅学习笔记——33.Network Compression(2_6)

3.为什么要pruning? 首先有一个问题:既然最后要得到一个小的network,那为什么不直接在数据集上训练小(有local minima的问题)的模型,而是先训练大模型?解释一:模型越大,越容易在数据集上找到一个局部最优解,而小模型比较难训练,有时甚至无法收敛。 解释二:2018年的…
暂无图片
编程学习 ·

Java 基础 A类集合存的数据B类调用

1.新建一个Callback抽象类public interface Callback {Map<String, Object> a() ; }2. b继承Callbackpublic class b implements Callback{@Overridepublic Map<String, Object> a() {Map<String,Object> map=new HashMap<>();map.put("china&q…
暂无图片
编程学习 ·

java常用面试题——笔试选择题解析

1.以下关于 abstract 关键字的说法,正确的是(D)。 A.abstract 可以与 final 并列修饰同一个类。 B.abstract 类中不可以有 private 的成员。 C.abstract 类中必须全部是 abstract 方法。 D.abstract 方法必须在 abstract 类或接口中。解析: final的类不能被重写和继承;而a…
暂无图片
编程学习 ·

windows10设置jdk环境变量

先安装好jdk 电脑–>属性–>高级系统设置–>新建 点击Path,编辑 添加这两项,确定 win+R打开cmd java -version检查jdk环境变量是否配置成功
暂无图片
编程学习 ·

驾考知识自查

驾考知识自查1事故的种类没有车辆追尾路段,只有事故多发路段2驾驶证 行驶证的换领驾驶证为核发地 行驶证为登记地3山坡挂挡问题p=fv 机动车功率一定 抵挡获得更大的牵引力,但不可以松开加速踏板,松开会导致遛坡4违规处罚问题饮酒后驾驶机动车的,处暂扣六个月机动车驾驶证,…
暂无图片
编程学习 ·

html中的锚点

一、页面内跳转的锚点设置页面内的跳转需要两步:方法一:①:设置一个锚点链接<a href="#miao">去找喵星人</a>;(注意:href属性的属性值最前面要加#)②:在页面中需要的位置设置锚点<a name="miao"></a>;(注意:a标签中要写…
暂无图片
编程学习 ·

深度学习:什么是backbone,benchmark,baseline

backbone:骨干网络,比如alexnet,ZFnet,VGG,googlenet...benchmark:性能指标,比如accuracy,内存消耗,模型复杂度。.baseline:对照组,也就是被用来对比的模型。比如resnet中用来对比的CNN就是baseline。
暂无图片
编程学习 ·

CTO也糊涂的常用术语:功能模块、业务架构、用户需求、文档……

功能模块、业务架构、需求分析、用户需求、系统分析、功能设计、详细设计、文档、业务、技术……很多被随口使用的名词,其实是含糊甚至错误的。到底含糊在哪里,错误在哪里,不仅仅是新手软件开发人员糊涂,许多入行多年的老手也一样。虽然很多老手功成名就,挂着CTO、总架构师…
暂无图片
编程学习 ·

分离springboot中的lib

仅供参考: 这次分离lib源于疫情期间连公司内网上传jar过于缓慢。在一个项目构建之初,所使用的依赖基本都已经确定,因此大概率不会对项目有大的影响。(非适用于所有场景) 首先我们要引入插件: <!-- 分离lib --> <plugin><groupId>org.apache.maven.plug…
暂无图片
编程学习 ·

MySQL数据库的备份与恢复(4)——mysqldump参数详解

MySQL数据库的备份与恢复(4)——mysqldump参数详解 mysqldump是MySQL自带的逻辑备份命令,备份文件包含一组SQL语句,可以通过执行这些语句来生成备份前的数据库对象定义和表数据。mysqldump命令还可以生成CSV,其他分隔文本或XML格式的输出。 mysqldump命令的格式如下: mys…
暂无图片
编程学习 ·

论文查找路径 查找IEEE论文 免费查看的方法

爱学术 深圳图书馆 IEEE IEEE转免费查看网站网址 论文免费查看 百度学术 百度学术搜索到的内容最全,但大部分都是其他网站的链接 查看IEEE的文章 可以先使用百度学术找到IEEE的链接(直接使用IEEE搜索反而有时候搜不到) 打开IEEE的链接,并复制 看看论文免费查看网站,将链接…
暂无图片
编程学习 ·

如何做到靠谱—传输层协议 TCP(下)

文章目录TCP 如何做到靠谱?如何实现一个靠谱的协议?顺序问题和丢包问题确认与重发机制流量控制问题拥塞控制问题小结 我们前面说到玄奘西行,要出网关。既然出了网关,那就是在公网上传输数据,公网往往是不可靠的,因而需要很多的机制去保证传输的可靠性,这里面需要恒心,也…