当前位置:

首页 > 编程开发 > Java基础之volatile关键字的应用实例分析

Java基础之volatile关键字的应用实例分析

问:请谈谈你对volatile的理解?答:volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:1)保证可见性2)不保证原子性3)禁止指令重排刚学完java基础,如果有人问你什么是volatile?它有什么作用的话,相信一定非常懵逼…可能看了答案,也完全不明白,什么是同步机制?什么是可见性?什么是原子性?什么是指令重排?1、volatile保证可见性1.1、什么是JMM模型?要想理解什么是可见性,首先要先理解JMM。JMM(Java内存模型,JavaMemoryModel)本身是一种抽象的

本文目录

    Java基础之volatile关键字的应用实例分析

    问:请谈谈你对volatile的理解?
    答:volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:
    1)保证可见性
    2)不保证原子性
    3)禁止指令重排

    刚学完java基础,如果有人问你什么是volatile?它有什么作用的话,相信一定非常懵逼…
    可能看了答案,也完全不明白,什么是同步机制?什么是可见性?什么是原子性?什么是指令重排?

    1、volatile保证可见性

    1.1、什么是JMM模型?

    要想理解什么是可见性,首先要先理解JMM。

    JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在。它描述的是一组规则或规范,通过这组规范,定了程序中各个变量的访问方法。JMM关于同步的规定:
    1)线程解锁前,必须把共享变量的值刷新回主内存;
    2)线程加锁前,必须读取主内存的最新值到自己的工作内存;
    3)加锁解锁是同一把锁;

    由于JVM运行程序的实体是线程,创建每个线程时,JMM会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。

    Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

    但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。

    看了上面对JMM的介绍,可能还是优点懵,接下来用一个卖票系统来进行举例:

    1)如下图,此时卖票系统后端只剩下1张票,并已读入主内存中:ticketNum=1。
    2)此时网络上有多个用户都在抢票,那么此时就有多个线程同时都在进行买票服务,假设此时有3个线程都读入了目前的票数:ticketNum=1,那么接着就会买票。
    3)假设线程1先抢占到cpu的资源,先买好票,并在自己的工作内存中将ticketNum的值改为0:ticketNum=0,然后再写回到主内存中。

    此时,线程1的用户已经买到票了,那么线程2,线程3此时应该不能再继续买票了,因此需要系统通知线程2,线程3,ticketNum此时已经等于0了:ticketNum=0。如果有这样的通知操作,你就可以理解为就具有可见性。

    Java基础之volatile关键字的应用实例分析

    通过上面对JMM的介绍和举例,可以简单总结下。

    JMM内存模型的可见性是指,多线程访问主内存的某一个资源时,如果某一个线程在自己的工作内存中修改了该资源,并写回主内存,那么JMM内存模型应该要通知其他线程来从新获取最新的资源,来保证最新资源的可见性。

    1.2、volatile保证可见性的代码验证

    在1.1中,已经基本理解了可见性的含义,接下来用代码来验证一下,volatile确实可以保证可见性。

    1.2.1、无可见性代码验证

    首先先验证下,不使用volatile,是不是就是没有可见性。

    package com.koping.test;import java.util.concurrent.TimeUnit;class MyData{
        int number = 0;
    
        public void add10() {
            this.number += 10;
        }}public class VolatileVisibilityDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            // 启动一个线程修改myData的number,将number的值加10
            new Thread(
                    () -> {
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 正在执行");
                        try{
                            TimeUnit.SECONDS.sleep(3);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        myData.add10();
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 更新后,number的值为" + myData.number);
                    }
            ).start();
    
            // 看一下主线程能否保持可见性
            while (myData.number == 0) {
                // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
                // 如果没有可见性的话,就会一直在循环里执行
            }
    
            System.out.println("具有可见性!");
        }}

    运行结果如下图,可以看到虽然线程0已经将number的值改为了10,但是主线程还是在循环中,因为此时number不具有可见性,系统不会主动通知。
    Java基础之volatile关键字的应用实例分析

    1.2.1、volatile保证可见性验证

    在上面代码的第7行给变量number添加volatile后再次测试,如下图,此时主线程成功退出了循环,因为JMM主动通知了主线程更新number的值了,number已经不为0了。
    Java基础之volatile关键字的应用实例分析

    2、volatile不保证原子性

    2.1 什么是原子性?

    理解了上面说的可见性之后,再来理解下什么叫原子性?

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

    还是有点抽象,接下来举个例子。

    如下图,创建了一个测试原子性的类:TestPragma。在add方法中将n加1,通过查看编译后的代码可以看到,n++被拆分为3个指令进行执行。

    因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。
    Java基础之volatile关键字的应用实例分析

    2.2 不保证原子性的代码验证

    在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

    首先给MyData类添加一个add方法

    package com.koping.test;class MyData {
        volatile int number = 0;
    
        public void add() {
            number++;
        }}

    然后创建测试原子性的类:TestPragmaDemo。测试下20个线程给number各加1000次之后,number的值是否是20000。

    package com.koping.test;public class TestPragmaDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
            for (int i=0; i<20; i++) {
                new Thread(() -> {
                    for (int j=0; j<1000; j++) {
                        myData.add();
                    }
                }).start();
            }
    
            // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
            while (Thread.activeCount()>2){
                Thread.yield();
            }
    
            System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
    
        }}

    运行结果如下图,最终number的值仅为18410。
    可以看到即使加了volatile,依然不保证有原子性。
    Java基础之volatile关键字的应用实例分析

    2.3 volatile不保证原子性的解决方法

    上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

    2.3.1 方法1:使用synchronized

    方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

    结果如下图,最终确实可以使number的值为20000,保证了原子性。

    但是,实际业务逻辑方法中不可能只有只有number++这1行代码,上面可能还有n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

    package com.koping.test;class MyData {
        volatile int number = 0;
    
        public synchronized void add() {
          // 在n++上面可能还有n行代码进行逻辑处理
            number++;
        }}

    Java基础之volatile关键字的应用实例分析

    2.3.2 方法1:使用JUC包下的AtomicInteger

    给MyData新曾一个原子整型类型的变量num,初始值为0。

    package com.koping.test;import java.util.concurrent.atomic.AtomicInteger;class MyData {
        volatile int number = 0;
    
        volatile AtomicInteger num = new AtomicInteger();
    
        public void add() {
            // 在n++上面可能还有n行代码进行逻辑处理
            number++;
            num.getAndIncrement();
        }}

    让num也同步加20000次。结果如下图,可以看到,使用原子整型的num可以保证原子性,也就是number++的时候不会被抢断。

    package com.koping.test;public class TestPragmaDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
            for (int i=0; i<20; i++) {
                new Thread(() -> {
                    for (int j=0; j<1000; j++) {
                        myData.add();
                    }
                }).start();
            }
    
            // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
            while (Thread.activeCount()>2){
                Thread.yield();
            }
    
            System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
            System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);
    
        }}

    Java基础之volatile关键字的应用实例分析

    3、volatile禁止指令重排

    3.1 什么是指令重排?

    在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

    计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
    源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

    处理器在进行重排时,必须要考虑指令之间的数据依赖性。

    单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

    但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。

    看了上面的文字性表达,然后看一个很简单的例子。
    比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
    1)1234
    2)2134
    3)1324
    以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

    public void mySort() {
        int x = 1;  // 语句1
        int y = 2;  // 语句2
        x = x + 3;  // 语句3
        y = x * x;  // 语句4}

    3.2 单线程单例模式

    看完指令重排的简单介绍后,然后来看下单例模式的代码。

    package com.koping.test;public class SingletonDemo {
        private static SingletonDemo instance = null;
    
        private SingletonDemo() {
            System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
        }
    
        public static SingletonDemo getInstance() {
            if (instance == null) {
                instance = new SingletonDemo();
            }
            return instance;
        }
    
        public static void main(String[] args) {
            // 单线程测试
            System.out.println("单线程的情况测试开始");
            System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
            System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
            System.out.println("单线程的情况测试结束\n");
        }}

    首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。
    Java基础之volatile关键字的应用实例分析

    3.3 多线程单例模式

    接下来在多线程情况下进行测试,代码如下。

    package com.koping.test;public class SingletonDemo {
        private static SingletonDemo instance = null;
    
        private SingletonDemo() {
            System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
        }
    
        public static SingletonDemo getInstance() {
            if (instance == null) {
                instance = new SingletonDemo();
            }
    
            // DCL(Double Check Lock双端检索机制)//        if (instance == null) {//            synchronized (SingletonDemo.class) {//                if (instance == null) {//                    instance = new SingletonDemo();//                }//            }//        }
            return instance;
        }
    
        public static void main(String[] args) {
            // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");
    
            // 多线程测试
            System.out.println("多线程的情况测试开始");
            for (int i=1; i<=10; i++) {
                new Thread(() -> {
                    SingletonDemo.getInstance();
                }, String.valueOf(i)).start();
            }
        }}

    在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。
    Java基础之volatile关键字的应用实例分析

    3.4 多线程单例模式改进:DCL

    在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

    package com.koping.test;public class SingletonDemo {
        private static SingletonDemo instance = null;
    
        private SingletonDemo() {
            System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
        }
    
        public static SingletonDemo getInstance() {//        if (instance == null) {//            instance = new SingletonDemo();//        }
    
            // DCL(Double Check Lock双端检锁机制)
            if (instance == null) {  // a行
                synchronized (SingletonDemo.class) {
                    if (instance == null) {  // b行
                        instance = new SingletonDemo();  // c行
                    }
                }
            }
            return instance;
        }
    
        public static void main(String[] args) {
            // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");
    
            // 多线程测试
            System.out.println("多线程的情况测试开始");
            for (int i=1; i<=10; i++) {
                new Thread(() -> {
                    SingletonDemo.getInstance();
                }, String.valueOf(i)).start();
            }
        }}

    在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。
    Java基础之volatile关键字的应用实例分析

    3.5 多线程单例模式改进,DCL版存在的问题

    需要注意的是3.4中的DCL版的单例模式依然不是100%准确的!!!

    是不是不太明白为什么3.4DCL版单例模式不是100%准确的原因?
    是不是不太明白在3.1讲完指令重排的简单理解后,为什么突然要讲多线程的单例模式?

    因为3.4DCL版单例模式可能会由于指令重排而导致问题,虽然该问题出现的可能性可能是千万分之一,但是该代码依然不是100%准确的。如果要保证100%准确,那么需要添加volatile关键字,添加volatile可以禁止指令重排。

    接下来分析下,为什么3.4DCL版单例模式不是100%准确?

    查看instance = new SingletonDemo();编译后的指令,可以分为以下3步:
    1)分配对象内存空间:memory = allocate();
    2)初始化对象:instance(memory);
    3)设置instance指向分配的内存地址:instance = memory;

    由于步骤2和步骤3不存在数据依赖关系,因此可能出现执行132步骤的情况。
    比如线程1执行了步骤13,还没有执行步骤2,此时instance!=null,但是对象还没有初始化完成;
    如果此时线程2抢占到cpu,然后发现instance!=null,然后直接返回使用,就会发现instance为空,就会出现异常。

    这就是指令重排可能导致的问题,因此要想保证程序100%正确就需要加volatile禁止指令重排。

    3.6 volatile保证禁止指令重排的原理

    在3.1中简单介绍了下执行重排的含义,然后通过3.2-3.5,借助单例模式来举例说明多线程情况下,为什么要使用volatile的原因,因为可能存在指令重排导致程序异常。

    接下来就介绍下volatile能保证禁止指令重排的原理。

    首先要了解一个概念:内存屏障(Memory Barrier),又称为内存栅栏。它是一个CPU指令,有2个作用:
    1)保证特定操作的执行顺序;
    2)保证某些变量的内存可见性;

    由于编译器和处理器都能执行指令重排。如果在指令之间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说,通过插入内存屏障,禁止在内存屏障前后的指令执行重排需优化。

    内存屏障的另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

    Java基础之volatile关键字的应用实例分析

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java volatile
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。