【JUC进阶】02. volatile关键字

news2024/11/17 10:28:50

目录

1、回顾JMM

1.1、可见性(Visibility)

1.2、原子性(Atomicity)

1.3、有序性(Ordering)

2、volatile

2.1、保证可见性

2.2、不保证原子性

2.3、防止指令重排

2.4、什么时候使用volatile

3、小结


1、回顾JMM

JMM(Java Memory Model)是Java内存模型的缩写,它定义了Java程序在多线程环境下内存访问的规则和语义。JMM的几个主要特性包括:可见性、原子性、有序性、顺序一致性。在我的《JVM内存模型》文章中,已经初步介绍了JMM相关特性,现在我们就来详细说说这些特性。

1.1、可见性(Visibility)

串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,在后续的步骤中读取这个变量的值时,读取的一定是修改后的新值。

可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。但是如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。如图:

如果有一个静态(共享)变量t,在 CPU1和CPU2 上各运行了一个线程,CPU1线程要读取变量t,CPU2线程要修改变量t。由于编译器优化或者硬件优化的缘故,在CPU1 上的线程将变量 t 进行了优化,将其缓存在 cache 中或者存器里。在这种情况下,如果在 CPU2 上的某个线程修改了变量t的实际值,那么CPU1 上的线程可能无法意识到这个改动,依然会读取 cache 中或者寄存器里的数据。因此,就产生了可见性问题。外在表现为:变量t的值被修改,但是 CPU1 上的线程依然会读到一个旧值。可见性问题也是并行程序开发中需要重点关注的问题之一。

例如如下代码:

public class VisibilityExample {
    private boolean t = false;

    public void updateFlag() {
        t = true;  // 修改共享变量t
    }

    public void printFlag() {
        while (!t) {
            // 空循环,等待t变为true
        }
        System.out.println("t is true");
    }
}

在上面的代码中,两个线程分别调用updateFlag和printFlag方法。由于没有同步机制,线程之间对于t的修改可能不可见,导致printFlag方法陷入死循环。

1.2、原子性(Atomicity)

原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

比如,对于一个静态全局变量 int i,两个线程同时对它赋值,线程A 给它赋值 i = 1,线程B 给它赋值为i = 2。那么不管这两个线程以何种方式、何种步调工作,i 的值要么是 1,要么是2。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。但如果我们不使用int型数据而使用 long 型数据,可能就没有那么幸运了。

package jmm;

/**
 * @author Shamee loop
 * @date 2023/6/17
 */
public class AtomicityDemo {

    private static long i;

    public static void main(String[] args) {
        // 线程赋值 1
        new Thread(() -> {
            while (true) {
                i = 111L;
                Thread.yield();
            }
        }, "thread-write-a").start();
        
        // 线程赋值 2
        new Thread(() -> {
            while (true) {
                i = -222L;
                Thread.yield();
            }
        }, "thread-write-b").start();
        
        // 线程赋值 3
        new Thread(() -> {
            while (true) {
                i = 333L;
                Thread.yield();
            }
        }, "thread-write-c").start();

        // 线程读取
        new Thread(() -> {
            while (true) {
                long tmp = i;
                if(tmp != 111L && tmp != -222L && tmp != 333L){
                    System.out.println("读取到的值:" + tmp);
                }
                Thread.yield();
            }
        }, "thread-read").start();
    }
}

上述代码中3个线程对long数据i进行赋值,分别赋值为111,-222,333。然后有一个线程进行读取i的值。通常来说,由于代码40行我们做了判断,那么41行代码是不会有内容输出的,也就是说i的是肯定是111,-222,333中的一个。事实上,当我们用64位的JDK运行时,并不会有任何问题。

注意控制台第一行是我的JDK版本信息。

而当我们使用32位JDK运行时:

我们看到读取到了相当多根本不存在的值。很多人可能应该想到了。

对于32位系统来说,long型数据的读写不是原子性的(因为 long 型数据有 64 位)。也就是说,如果两个线程同时对long数据进行写入或读取,则对线程之间的结果是会产生干扰的。

因为计算组存储的数据是二进制,因此这些数字都会转化成二进制数据。我们可以将上面的几个相关数字算出他们的补码。就会发现,4294967074等数字是111的前32位和-222的后32位数字合并而成的。也就是说,由于线程并行的关系,数字被乱写了。 而long类型64位,这就导致了读的时候也串了。

这个例子便是我们所说的原子性。

1.3、有序性(Ordering)

对于一个线程的执行代码而言,我们总是习惯性地认为代码是从前往后依次执行的。这么理解也不能说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人的直观感觉就是: 写在前面的代码,会在后面执行。听起来有些不可思议,是吗? 有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。

package jmm;

/**
 * @author Shamee loop
 * @date 2023/6/17
 */
public class OrderingDemo {

    int a = 0;
    boolean flag = false;

    public void writer(){
        a = 1;
        flag = true;
    }

    public void reader(){
        if(flag){
            int i = a + 1;
            // ...
        }
    }
}

假设线程A先执行了writer() 方法,接着线程B执行了reader()方法。如果发生指令重排,线程B在19行读取的时候,不一定能看到a被成功赋值了。当然,这里说的不是绝对的,前提是如果有发生指令重排的情况下。

因此,对于一个线程来说,他看到的指令执行顺序一定是一致的,也就是说指令重排有一个基本前提,就是保证穿行语义的一致性。

但是并发编程中,就没有义务保证多线程间的语义一致性。

那么,为什么要指令重排?

为了提高程序的性能和优化执行效率。在现代处理器中,存在多级缓存和乱序执行等优化技术,指令重排是其中的一种。指令重排是指编译器或处理器在保持程序执行结果不变的前提下,重新排序指令的执行顺序。它可以通过优化指令的执行顺序,减少处理器的空闲时间,提高指令级并行性和性能。

2、volatile

前面已经简单介绍了JMM,java内存模型都是围绕着原子性,可见性,有序性展开的。而前面我们也介绍到了,不遵循这些特性,以及发生指令重排情况下,可能会有超出期望的情况发生。

为了在适当的场合,确保线程间的有序性、可见性和原子性。Java 使用了一些特殊的操作或者关键字来声明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字 volatile 就是其中之一。volatile英译:不稳定的,顾名思义。

当你用关键字 volatile 声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。

比如,根据编译器的优化规则,如果不使用关键字 volatile 声明变量,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改顺序都会是反的。一旦使用关键字 volatile,虚拟机就会特别小心地处理这种情况。

2.1、保证可见性

  • volatile关键字保证了被修饰变量的可见性,即当一个线程修改了该变量的值,其他线程能够立即看到最新的值。
  • 普通的变量在多线程环境下存在线程间的不可见性问题,即一个线程修改了变量的值,其他线程无法立即看到修改后的值,而需要通过同步机制(如锁)来保证可见性。
  • 使用volatile修饰的变量可以确保对它的写操作对其他线程可见,从而避免了线程间的数据不一致性问题。

针对上面原子性中的代码示例,使用关键字 volatile 进行调整:

// 只需要在变量前声明volatile关键字
private volatile static long i;

执行结果:

2.2、不保证原子性

volatile不能保证原子性,也不能代替锁,它也无法保证一些复合操作的原子性。比如下面的例子,通过关键字 volatile 是无法保证 i++的原子性操作的。

/**
 * @author Shamee loop
 * @date 2023/3/24
 */
public class VolatileDemo {

    public static volatile int num = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new AddThread());
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }

        System.out.println("sum:" + num);
    }

    public static class AddThread implements Runnable {

        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                num++;
            }
        }
    }
}

上述代码中,累计10个线程对i进行累加;每个线程累加10000次,如果是原子性的,那么应该输出是100000。但是看下输出结果:

每次都小于100000。

2.3、防止指令重排

什么是内存屏障?

内存屏障(Memory Barrier),也称为内存栅栏或屏障指令,是一种硬件或软件机制,用于控制指令的执行顺序和内存访问的顺序,保证内存操作的有序性和可见性。内存屏障在多线程编程中起到重要的作用,确保程序的正确性和一致性。

  1. 保证指令的顺序性:内存屏障可以限制指令的执行顺序,确保指令按照程序的顺序进行执行。在内存屏障之前的指令会先于内存屏障之后的指令执行,不会发生乱序执行的情况。这样可以避免指令重排带来的问题,保证程序的正确性。
  2. 保证内存操作的有序性:内存屏障可以控制对内存的读写操作顺序,确保内存操作按照程序的顺序进行。在内存屏障之前的内存操作会先于内存屏障之后的内存操作执行,不会发生乱序访问内存的情况。这样可以避免由于内存操作顺序不一致而引起的数据不一致性问题。
  3. 保证内存可见性:内存屏障可以确保对共享变量的修改对其他线程立即可见。当一个线程在内存屏障之前对共享变量进行写操作时,会将修改后的值刷新到主内存中;而其他线程在内存屏障之后读取该共享变量时,会从主内存中获取最新的值。这样可以保证多线程环境下的共享变量的可见性,避免了数据的不一致性问题。

在volatile变量的读写操作前后会插入内存屏障,确保写入操作先于读取操作,从而避免了指令重排带来的问题。这样可以保证多线程环境下的可见性和一致性,确保变量的修改对其他线程立即可见。

因此:

  • volatile关键字禁止了编译器和处理器对被修饰变量操作的重排序,保证了指令执行的有序性。
  • 在多线程环境下,指令重排序可能导致线程间的结果不一致性。使用volatile修饰的变量能够确保变量的读写操作按照程序的顺序执行,避免了潜在的线程安全问题。

2.4、什么时候使用volatile

当一个变量被多个线程并发访问和修改时,应该使用Java中的volatile关键字,并且它的值需要对所有线程实时可见。以下是一些适合使用 volatile 关键字的场景:

  • 状态标志:如果你有指示进程状态的标志,例如当前正在运行的任务,你可能希望将其标记为易变的。这将确保所有线程都可以看到进程的更新状态。
  • 计数器:如果你有一个由多个线程递增的计数器,你可能希望将其标记为易变的。这将确保所有线程都可以看到计数器的当前值并避免竞争条件。
  • 配置变量:如果你有一个用于配置系统行为的变量,例如超时值或最大连接数,你可能希望将其标记为易变的。Java 中 volatile 关键字的使用将确保所有线程都可以看到配置变量的当前值并相应地调整它们的行为。

3、小结

volatile尤其要注意的是,他能保证可见性和防止指令重排,但是并不能保证原子性。如果需要保证原子性操作,可以使用原子类(AtomicInteger)或加锁机制来代替volatile。通常我们在创建单例的时候,会使用volatile+双重检查锁来确保线程安全,便是这个道理。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微服务中「组件」集成

有品&#xff1a;There is no silver bullet&#xff1b; 一、简介 在微服务工程的技术选型中&#xff0c;会涉及到很多组件的集成&#xff0c;最常用包括&#xff1a;缓存、消息队列、搜索、定时任务、存储等几个方面&#xff1b; 如果工程是单服务&#xff0c;对于集成组件…

有趣的数学 关于自然常数e

一、e的值 自然常数&#xff08;也称欧拉数&#xff09;e是数学中最重要的数字之一。 2.7182818284590452353602874713527...... 二、从复利理解e 设想你在一家银行有一个银行账户&#xff0c;该银行付给你一个慷慨的利息年利率12%,一年计一次复利&#xff0e;你将一笔初始存款…

测试(二)

1.软件测试的生命周期 需求分析→测试计划→ 测试设计→ 测试开发→ 测试执行→ 测试评估 2.如何描述一个Bug 3.Bug的优先级 1、Blocker&#xff08;崩溃&#xff09;&#xff1a; 阻碍开发或测试工作的问题&#xff1b;造成系统崩溃、死机、死循环&#xff0c;导致数据库数…

Windows Server 2016 OVF, updated Jun 2023 (sysin) - VMware 虚拟机模板

2023 年 6 月版本更新&#xff0c;现在自动运行 sysprep&#xff0c;支持 ESXi Host Client 部署 请访问原文链接&#xff1a;https://sysin.org/blog/windows-server-2016-ovf/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org…

Kubernetes 纯理论 贼干篇

Kubernetes理论 docker 容器引擎 docker compose 单机编排工具 docker swarm Docker容器多机编排工具&#xff0c;实现Docker容器的集群管理调度的工具 k8s 容器多机编排工具&#xff0c;占据80%以上的市场份额 mesos marathon mesos:分布式资管管理框架&#xff0c;可以对…

2019年全国硕士研究生入学统一考试管理类专业学位联考写作试题

写作:第56&#xff5e;57小题&#xff0c;共65分。其中论证有效性分析30分&#xff0c;论说文35分。 56&#xff0e;论证有效性分析 分析下述论述中存在的缺陷和漏洞&#xff0c;选择若干要点&#xff0c;写一篇600字左右的文章&#xff0c;对论证的有效性进行分析和评论。(论…

Linux终端与进程的关系 ( 1 ) -【Linux通信架构系列】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…

案例:从定性原因分析上升到定量原因分析

在定量原因分析时&#xff0c;主要是有四种定量思考的方法&#xff1a; 1、数据的居中趋势与离散程度分析&#xff1a;均值、标准差 2、 80-20分析&#xff1a;在所有的构成成分中&#xff0c;哪个成分占比最大 3、数据的相关性分析&#xff1a;是否存在强相关 4、敏感性分…

[进阶]Java:文件字符输入流、文件字符输出流

问&#xff1a;字节流读取中文输出可能会存在什么问题&#xff1f; 会乱码。或者内存溢出。 读取中文输出&#xff0c;哪个流更合适&#xff0c;为什么&#xff1f; 字符流更合适&#xff0c;最小单位是按照单个字符读取的。 代码演示如下&#xff1a; public class FileR…

[C++]vs2019运行c++报错:错误 C1075 “{”: 未找到匹配令牌

源码是从git拉下来的&#xff0c;但是我并没有改任何东西&#xff0c;结果报错超过100个&#xff0c;这个很明显不是代码问题&#xff0c;最后发现需要把LF换成CRLF&#xff0c;修改方法很简单&#xff0c;就是VS2019打开源代码右下角切换即可。如图 错误原因就是github下载的源…

【MySQL】不就是MySQL——多表查询

前言 嗨&#xff01;小伙伴们大家好呀&#xff0c;忙碌的一周就要开始&#xff01;在此之前我们学习的MySQL数据库的各种操作都是在一张表之中&#xff0c;今天我们学习要对多张表进行相关操作&#xff0c;相比较于单一的表来说&#xff0c;多张表操作相对复杂一些&#xff0c;…

【数据库三】数据库的存储引擎

存储引擎 1.存储引擎1.1 概念介绍1.2 常用存储引擎 2.MyISAM2.1 特点介绍2.2 支持的存储格式2.3 适用的生产场景 3.InnoDB3.1 特点介绍3.2 适用生产场景分析4.企业选择存储引擎依据 5.MyISAM和InnoDB的区别命令操作 1.存储引擎 1.1 概念介绍 MySQL数据库中的组件&#xff0c;负…

深层神经网络

1、深层网络中的前向传播 一个训练样本 x 前向传播 第一层需要计算 &#x1d467; [1] &#x1d464;[1]&#x1d465; &#x1d44f; [1]&#xff0c;&#x1d44e; [1] &#x1d454; [1] (&#x1d467; [1] )&#xff08;&#x1d465;可以看做 &#x1d44e; [0] &am…

软件工程导论期末救急包(中)

目录 用户需求 需求分析常用的分析方法 软件设计 创建良好设计的原则 内聚性 耦合性 UML中各种视图及其作用 用例视图VS逻辑视图 UML中的主要图及其作用 软件开发过程与UML可视化建模 MVC模式 MVVM模式 面向对象模型主要哪些模型组成&#xff1f; 概要设计阶段的基本任务是什…

联想小新 Pro 16 2022 锐龙版开启S3睡眠模式方法,解决S0睡眠发热耗电容易唤醒等烦恼

联想小新 Pro 16 2022 锐龙版&#xff0c;CPU为AMD Ryzen 7 6800H 默认为S0睡眠模式&#xff0c;经常发热睡死过去&#xff0c;并且无法禁止鼠标唤醒电脑 本文借助“Universal AMD Form Browser”软件&#xff0c;在固件层面修改笔记本的睡眠方式。 本文所用到的软件Univer…

【Unity】 基础入门 编译错误排查与调试方法

基础入门 编译错误排查与调试方法 一、常见编译错误原因1、环境问题2、代码命名问题二、代码调试方法1、基础调试方法2、高级玩法3、unity调试工具插件一、常见编译错误原因 1、环境问题 1、Win11系统不兼容部分unity版本 考虑换系统吧! 2、可能是系统权限问题,访问不到部分…

jmeter工具介绍

Jmeter性能测试工具介绍 Jmeter的背景介绍Jemter过程类元件介绍Jmeter结果查看类元件介绍Jmeter其他介绍 Jmeter背景介绍&#xff1a; Apache JMeter是Apache组织的开放源代码项目&#xff0c;是一个100%纯Java桌面应用&#xff0c;用于压力测试和性能测试。它最初被设计用于…

C++技能系列 ( 2 ) - const的几种使用【详解】

系列文章目录 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 C技能系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 生活就是上帝发给你的一张手牌&#xff0c;无论多烂&#xff0c;你都得拿着。 Life is god give you a hand, no matter ho…

【前端面试手册】CSS系列-01

本专栏收录于《前端面试手册-CSS篇》如果该文章对您有帮助还希望你能点一个小小的订阅&#xff0c;来增加博主创作的动力✍&#x1f3fb; 一、什么是盒子模型&#xff0c;说说你的理解&#xff1f; 首先&#xff0c;如果你要对一个文档进行布局的时候&#xff0c;浏览器的渲染…

[进阶]Java:对象序列化、反序列化

对象序列化&#xff1a; 使用到的流是对象字节输出流&#xff1a;ObjectOutputStream作用&#xff1a;以内存为基础&#xff0c;把内存中的对象存储到磁盘文件中去。称为对象序列化。 代码演示如下&#xff1a; 学生类&#xff1a; /**对象如果要序列化一定要实现Serializab…