volatile 详解

目录

一. 前言

二. 可见性

2.1. 可见性概述

2.2. 内存屏障

2.3. 代码实例

三. 不保证原子性

3.1. 原子性概述

3.2. 如何解决 volatile 的原子性问题呢?

四. 禁止指令重排

4.1. volatile 的 happens-before 关系

4.2. 代码实例

五. volatile 应用场景

5.1. 状态标志

5.2. 一次性安全发布(one-time safe publication)

5.3. 独立观察(independent observation)

5.4. volatile bean 模式

5.5. 开销较低的读-写锁策略

5.6. 双重检查(double-checked)


一. 前言

    volatile 可以看做是轻量级的 synchronized,它只保证了共享变量的可见性,是Java虚拟机提供的轻量级的同步机制。在线程 A 修改被 volatile 修饰的共享变量之后,线程 B 能够读取到正确的值。Java 在多线程中操作共享变量的过程中,会存在指令重排序与共享变量工作内存缓存的问题。volatile 一共有三大特性,保证可见性不保证原子性禁止指令重排

二. 可见性

2.1. 可见性概述

    首先提一个JMM的概念,JMM是Java内存模型,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。JMM规定所有的变量都在主内存,主内存是公共的,所有线程都可以访问,线程对变量的操作必须是在自己的工作内存中。在这个过程中可能出现一个问题。

现在假设主物理内存中存在一个变量,他的值为7,现在线程A和线程B要操作这个变量,所以他们首先要将这个变量的值拷贝一份放到自己的工作内存中,如果A将这个值改为1,这时候线程B要使用这个变量但是B线程工作内存中的变量副本是7 不是新修改的1 这就会出现问题。

所以JMM规定线程解锁前一定要将自己工作内存的变量写回主物理内存中,线程加锁前一定要读取主物理内存的值。也就是说一旦线程A修改了变量值,线程B马上能知道并且能更新自己工作内存的值。这就是可见性。 

2.2. 内存屏障

volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。

内存屏障又称内存栅栏,是一个 CPU 指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止 + 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

2.3. 代码实例

设计思路:首先,我们新建一个类,里面有一个 number,然后写一个方法可以让他的值变成60,这时候在主线程中开启一个新的线程让他 3s 后将number值改为60,然后主线程写一个循环如果主线程能立刻监听到number值的改变则主线程输出改变后的值,此时说明有可见性。如果一直死循环,说明主线程没有监听到number值的更改,说明不具有可见性。

class MyData {
    public int number = 0;

    public void change() {
        number = 60;
    }
}

public class VolatileTest {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "number is :"+ myData.number);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.change();
            System.out.println(Thread.currentThread().getName() + "has changed :" + myData.number);
        }, "A").start();
        while(myData.number == 0){

        }
        System.out.println(Thread.currentThread().getName() + "number is:" + myData.number);
    }
}

看一下结果:

结果是进入了死循环一直空转,说明不具有可见性。下面我们在number前面加上关键字volatile。 public volatile int number = 0;

证明能监控到number值已经修改,说明加上volatile具有可见性。

三. 不保证原子性

3.1. 原子性概述

    原子性指的是不可分割,完整性,也即某个线程正在做某个业务时不能被分割,要么同时成功,要么同时失败。

    为了证明 volatile 能不能保证原子性,我们可以通过一个案例来证明一下。首先我们在之前的MyData 类中加入一个方法 addplus() 能让number加1,然后我们创建20个线程,每个线程调用1000次 addplus()。看看结果,如果number是20000,那么他就能保证原子性,如果不是20000,那么就不能保证原子性。

class MyData{
    public static volatile int number = 0;
	
    public void change(){
        number = 60;
    }
	
    public void incre(){
        number++;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {
        MyData data = new MyData();
		
        for (int i = 0; i < 20; i++) {
            new Thread(()-> {
                for (int j = 0; j < 1000; j++) {
                    data.incre();
                }
            }, String.valueOf(i)).start();
        }
		
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number is: " + MyData.number);
    }
}

结果不是20000,说明不能保证原子性。 

不保证原子性的原因:number++ 这个操作一共有3步,第一步从主物理内存中拿到number的值,第二步 number + 1,第三步写回主物理内存。

假设一开始主物理内存的值为0,线程A、线程B分别读取主物理内存的值到自己的工作内存,然后执行加1操作。这时候按理说线程A 将1写回主物理内存,然后线程B 读取主物理内存的值然后加1变成2,但是在线程A写回的过程中突然被打断线程A挂起,线程B 将1写回主物理内存这时候线程A重新将1写回主物理内存最终主物理内存的值为1,两个线程加了两次最后值居然是1,出错了。 

3.2. 如何解决 volatile 的原子性问题呢?

    我们需要使用原子类,原子类是保证原子性的。加入一个 AtomicInteger 类的成员,然后调用他的getAndIncrement() 方法(就是把这个数加1,底层用CAS保证原子性)。原子类的具体讲解请参见《JUC之Atomic原子类》。

运行结果:

这就解决了不保证原子性的问题。 

四. 禁止指令重排

    禁止指令重排又叫保证有序性。计算机编译器在执行代码的时候不一定非得按照你写代码的顺序执行。他会经历编译器优化的重排,指令并行的重排,内存系统的重排,最终才会执行指令,多线程环境更是如此,可能每个线程代码的执行顺序都不一样,这就是指令重排。

4.1. volatile 的 happens-before 关系

happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

// 假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    
    public void writer() {
        a = 1;              // 1 线程A修改共享变量
        flag = true;        // 2 线程A写volatile变量
    } 
    
    public void reader() {
        if (flag) {         // 3 线程B读同一个volatile变量
        int i = a;          // 4 线程B读共享变量
        ……
        }
    }
}

根据 happens-before 规则,上面过程会建立 3 类 happens-before 关系。
1. 根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
2. 根据 volatile 规则:2 happens-before 3。
3. 根据 happens-before 的传递性规则:1 happens-before 4。

因为以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。 

4.2. 代码实例

public class VolatileReSort {
    int a = 0;
    boolean flag = false;

    public void methodA() { // 线程A
        a = 1;
        flag = true;
    }

    public void methodB() { // 线程B
        if(flag) {
            a = a + 5;
            System.out.println("**********retValue:" + a);
        }
    }
}

假设现在线程A、线程B 分别执行上面两个方法,由于指令的重排序,可能线程A中的两条语句发生了指令重排,flag先变为true,这时候线程B突然进来判断flag为true,然后执行下面的最后输出结果为a = 5,但是也有可能先执行a = 1,那这样结果就是a = 6,所以,由于指令重排可能导致结果出现多种情况。现在加上volatile关键字,他会在指令间插入一条Memory Barrier,来保证指令按照顺序执行不被重排。

五. volatile 应用场景

5.1. 状态标志

    也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

5.2. 一次性安全发布(one-time safe publication)

    缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。这就是造成著名的双重检查锁定【double-checked-locking】问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

5.3. 独立观察(independent observation)

    安全使用 volatile 的另一种简单模式是定期发布观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

5.4. volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}

5.5. 开销较低的读-写锁策略

    volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        return value++;
    }
}

5.6. 双重检查(double-checked)

传统的单例模式,在单线程下其实是没有什么问题的,多线程条件下就不行了。

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + " :构造方法被执行");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

可以看出多线程下单例模式将会失效。我们通过DCL双重检查锁可以解决上述问题。 

public static SingletonDemo getInstance() {
	if(instance == null) {
		synchronized (SingletonDemo.class) {
			if(instance == null) {
				instance = new SingletonDemo();
			}
		}
	}
	return instance;
}

但是这种方式也有一定的风险。原因在于某一个线程执行到第一次检测,读取到的 instance 不为null 时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo(); 可以分为以下3步完成:1. 分配对象内存空间;2. 初始化对象;3. 设置instance指向刚分配的内存地址,此时instance!=null。但是由于编译器优化可能会对2、3两步进行指令重排也就是先设置instance指向刚分配的内存地址,但是这时候对象还没有初始化,如果这时候新来的线程调用了这个方法就会发现instance != null 然后就返回 instance,实际上 instance 没被初始化,也就造成了线程安全的问题。为了避免这个问题我们可以使用 volatile 对其进行优化,禁止他的指令重排就不会发生上述问题了,给变量加上 volatile,如 private static volatile SingletonDemo instance = null;。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/181320.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JOSEF 漏电继电器 LLJ-100FG φ45 50-500mA 卡轨安装

系列型号&#xff1a; LLJ-10F(S)漏电继电器LLJ-15F(S)漏电继电器LLJ-16F(S)漏电继电器 LLJ-25F(S)漏电继电器LLJ-30F(S)漏电继电器LLJ-32F(S)漏电继电器 LLJ-60F(S)漏电继电器LLJ-63F(S)漏电继电器LLJ-80F(S)漏电继电器 LLJ-100F(S)漏电继电器LLJ-120F(S)漏电继电器LLJ-125F(S…

Linux基础命令4

find查找操作 1.文件名 上图中&#xff0c;一共有4个部分&#xff0c;分别是find&#xff0c;搜索路径&#xff0c;-name&#xff0c;文件名 find加上文件的路径&#xff08;也就是要查找的文件在根目录下的usr目录下的bin目录底下&#xff09; 加上 -name 加上文件名&a…

如何用网格交易做ETF套利

ETF套利是指利用ETF基金的交易机制&#xff0c;通过短期的买卖差价或组合投资来获取利润。 具体来说&#xff0c;ETF套利最常用的套利方法则是&#xff1a;价格套利和波动套利。 1. 价格套利&#xff1a;当ETF二级市场的价格与一级市场的净值出现偏差时&#xff0c;投资者可以通…

消息中间件——RabbitMQ(五)快速入门生产者与消费者,SpringBoot整合RabbitMQ!

前言 本章我们来一次快速入门RabbitMQ——生产者与消费者。需要构建一个生产端与消费端的模型。什么意思呢&#xff1f;我们的生产者发送一条消息&#xff0c;投递到RabbitMQ集群也就是Broker。 我们的消费端进行监听RabbitMQ&#xff0c;当发现队列中有消息后&#xff0c;就进…

CS2的到来会对csgo产生什么影响?

从左手持枪到教练观战位&#xff0c;周四更新的CS新版本缺乏CSGO里很多关键功能。社区服务器和创意工坊地图&#xff0c;目前最重要的功能缺失是创意工坊地图和社区服务器。这些社区制作的地图长期以来一直是玩家磨练技能的首选场所&#xff0c;从死斗服务器到用来练习瞄准、跑…

动态loading

项目中需要用到动图loading的地方可以下载 https://www.intogif.com/loading/ 高级点的还有css动画;692 Loaders: CSS & Tailwind 692 Loaders: CSS & Tailwind

【带头学C++】----- 八、C++面向对象编程 ---- 8.1 面向对象编程概述

目录 8.1 面向对象编程概述 8.1.1 面向对象概念&#xff08;OOP&#xff09; 8.1.2 面向过程概念 8.1 面向对象编程概述 8.1.1 面向对象概念&#xff08;OOP&#xff09; 面向对象&#xff08;Object-Oriented&#xff09;是一种编程范式&#xff0c;它将程序设计中的数据和…

section header

section header table 是一个section header的集合&#xff0c;每个section header是一个描述section的结构体。在同一个ELF文件中&#xff0c;每个section header大小是相同的。 每个section都有一个section header描述它&#xff0c;但是一个section header可能在文件中没有…

创新建筑形式:气膜体育馆助力校园体育设施革新

体育场馆在校园中扮演着重要的角色&#xff0c;是学生们进行体育锻炼、比赛和各类体育活动的场所。传统的室内体育馆建设往往需要大量资金和漫长的建设周期&#xff0c;但随着气膜体育馆的崭露头角&#xff0c;校园体育设施的面貌正迎来一场革新。 快速搭建&#xff0c;灵活性极…

虚拟机系列:windows 虚拟机相关功能、组件梳理

一. 简介 英文名称中文名称说明Container容器Guarded Host受保护的主机利用远程证明创建并运行受防护的虚拟机Hyper-V├Hyper-V Management ToolsHyper-V 管理工具包含 GUI 管理工具和 Power Shell 的 Hyper-V 模块└Hyper-V PlatformHyper-V 平台├Hyper-V HypervisorHyper-V …

浅谈JDK动态代理(上)

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 到目前为止&#xff0c…

基于 Flink SQL 和 Paimon 构建流式湖仓新方案

本文整理自阿里云智能开源表存储负责人&#xff0c;Founder of Paimon&#xff0c;Flink PMC 成员李劲松在云栖大会开源大数据专场的分享。本篇内容主要分为四部分&#xff1a; 数据分析架构演进介绍 Apache PaimonFlink Paimon 流式湖仓流式湖仓Demo演示 数据分析架构演进 …

Flutter 父子组件通信

在Flutter 中父组件调用子组件的方法可以通过GlobalKey实现&#xff0c;而子组件调用父组件方法可以通过回调函数实现。 父组件 class _MyHomePageState extends State<MyHomePage> {final GlobalKey<LoadPencilState> loadPencilKey GlobalKey<LoadPencilSt…

在 Python 的 requests 二进制数据的传输方式发生了变化

在Python编程中&#xff0c;requests库是一个非常有用的工具&#xff0c;用于发送HTTP请求。由于其简单易用的API和广泛的兼容性&#xff0c;requests库已经成为Python开发者中最常用的网络请求库之一。 然而&#xff0c;最近在requests 0.10.1版本中&#xff0c;POST二进制数据…

课堂巡课如何提升教学质量?简单才是硬道理

随着教育技术的不断发展&#xff0c;在线巡课系统逐渐成为学校管理和教育质量提升的重要工具。在线巡课系统通过数字化手段&#xff0c;为学校提供了更加高效、精准的巡课管理方式&#xff0c;有力地支持了教育教学的改进和优化。 客户案例 小学巡课项目 山东某小学引入了泛地…

python练习题

1.身体质量指数 BMI指数即身体健康指数&#xff0c;它与人的体重和身高相关&#xff0c;是目前国际常用的衡量人体胖瘦程度以及是否健康的一个标准。已知BMI值的计算公式如下&#xff1a; 体质指数&#xff08;BMI&#xff09; 体重&#xff08;kg&#xff09;身高^2&#xf…

基于qemu_v8+optee 3.17平台的ca/ta Demo

1、整体集成构建 基于官方构建&#xff0c;加入自定义ca/ta后一体构建到rootfs&#xff0c;在qemu上运行 $ mkdir -p <optee-project> $ cd <optee-project> $ repo init -u https://github.com/OP-TEE/manifest.git -m ${TARGET}.xml [-b ${BRANCH}] $ repo syn…

IDEA的插件市场无法打开,无法连接到https://plugins.jetbrains.com/

1&#xff1a;网上搜到的&#xff1a; 在这里测试https://plugins.jetbrains.com/ 能否连接到&#xff0c;可以的话就成功&#xff0c;但是我一直失败&#xff0c;网络配置与防火墙也没问题。 2&#xff1a;我成功的方法&#xff1a; 把这个勾取消再测试&#xff0c;成功&…

【Linux】make/Makefile 进度条小程序

目录 一&#xff0c;认识 make/makefile 二&#xff0c;实例代码 1&#xff0c;依赖关系 2&#xff0c;原理 3&#xff0c;项目清理 4&#xff0c;测试讲解 三&#xff0c;Linux第一个小程序&#xff0d;进度条 game.h game.c test.c 程序详解 一&#xff0c;认识 m…

Notepad-- ubuntu下载安装

Notepad-- ubuntu下载安装 下载 Gitee链接&#xff1a; https://gitee.com/cxasm/notepad– 安装 sudo apt install *.deb运行 /opt/apps/com.hmja.notepad/files/Notepad--出错 需要安装qt5 sudo apt-get install qt5-default