目录
1、volatile保证可见性
1.1、什么是JMM模型?
1.2、volatile保证可见性的代码验证
1.2.1、无可见性代码验证
1.2.1、volatile保证可见性验证
2、volatile不保证原子性
2.1 什么是原子性?
2.2 不保证原子性的代码验证
2.3 volatile不保证原子性的解决方法
2.3.1 方法1:使用synchronized
2.3.2 方法1:使用JUC包下的AtomicInteger
3、volatile禁止指令重排
3.1 什么是指令重排?
3.2 单线程单例模式
3.3 多线程单例模式
3.4 多线程单例模式改进:DCL
3.5 多线程单例模式改进,DCL版存在的问题
3.6 volatile保证禁止指令重排的原理
首页 Java java教程 Java基础之volatile应用实例分析

Java基础之volatile应用实例分析

May 28, 2023 am 11:23 AM
java volatile

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正在执行第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。验证number的值是否为20000,需要测试通过20个线程分别对其加1000次后的结果。

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++的单行代码,通常会包含其他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应用实例分析

以上是Java基础之volatile应用实例分析的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Java 中的平方根 Java 中的平方根 Aug 30, 2024 pm 04:26 PM

Java 中的平方根指南。下面我们分别通过例子和代码实现来讨论平方根在Java中的工作原理。

Java 中的完美数 Java 中的完美数 Aug 30, 2024 pm 04:28 PM

Java 完美数指南。这里我们讨论定义,如何在 Java 中检查完美数?,示例和代码实现。

Java 中的随机数生成器 Java 中的随机数生成器 Aug 30, 2024 pm 04:27 PM

Java 随机数生成器指南。在这里,我们通过示例讨论 Java 中的函数,并通过示例讨论两个不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。这里我们通过示例讨论简介、如何使用weka java、平台类型和优点。

Java 中的史密斯数 Java 中的史密斯数 Aug 30, 2024 pm 04:28 PM

Java 史密斯数指南。这里我们讨论定义,如何在Java中检查史密斯号?带有代码实现的示例。

Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

See all articles