Java并发编程-volatile

这篇具有很好参考价值的文章主要介绍了Java并发编程-volatile。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

volatilejava虚拟机提供的一种轻量级的同步机制,它有三个重要的特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

要理解这三个特性,就需要对JMM(JAVA内存模型)有一定的了解才行。

主要解决的问题:
JVM中,每个线程都会存在本地内存,本地内存是公共内存的副本,各个线程的本地内存相互隔离,就会存在一个线程对共享变量做了修改,其他线程没有感知到的情况,从而导致数据不一致

一、JMM(JAVA内存模型)

JMMJava 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMMJVM 中定义的一种并发编程的底层模型机制。JMM定义了线程和主内存(可以理解为买电脑时8/16G内存)之间的抽象关系:不同线程之间的共享变量存在主内存中,而每个线程中存在一个私有的本地内存,对共享变量的操作需要将主内存中的共享变量拷贝一份到本地内存中。也就是说,在每个线程的本地内存中存在的是共享变量的副本。

JMM关于同步的规定:

  • 1、线程解锁前,必须把共享变量的值刷新会主内存
  • 2、线程加锁前,必须读取主内存中的最新共享变量的值到本地内存
  • 3、加解锁是同一把锁

每个线程在创建时JVM都会为其分配工作内存(也叫栈空间),工作内存是每个线程的私有区域。而java内存模型规定所有变量都必须存在主内存中,主内存是共享区域,所有线程都可以访问。但是线程对变量的操作必须在工作内存中进行,大概流程就是,线程将变量的值从主内存拷贝到本地内存中,进行操作,然后在将其写回主内存。由于不同线程之间的工作内存互不可见,所有线程中的通信必须通过主内存来进行。具体过程如下:

由于JMM这样的机制,就导致了可见性的问题。

JMM三大特性

  • 可见性
  • 原子性
  • 有序性

二、可见性

内存可见性指当一个线程修改了某个变量的值后,其他线程总能知道这个值的变化。

这里用例子来说明一下:

package com.fzkj.juc;

import java.util.concurrent.TimeUnit;

/**
 * @DESCRIPTION  volatile关键字测试类
 */
public class VolatileTest {

    public static void main(String[] args) {
        Number number = new Number();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            number.numTo(20);
            System.out.println(Thread.currentThread().getName() + ":\t number的值是: " + number.num);
        }, "A线程").start();

        while(number.num == 0){}

        System.out.println(Thread.currentThread().getName() + ":\t number的值是: " + number.num);
    }


}

class Number{
    int num = 0;

    public void numTo(int target){
        this.num = target;
    }

    public void add(){
        this.num++ ;
    }
}

运行上面的例子就会发现,程序会陷入死循环,永远不会输出最后一句话。就是因为A线程中对变量num的修改对main线程不可见,导致while循环一直进行。

可见性问题常见的解决方案包括:

  • 加锁
  • volatile关键字

volatile

对上面代码进行改造

class Number{
    volatile int num = 0;

    public void numTo(int target){
        this.num = target;
    }

    public void add(){
        this.num++ ;
    }
}

这样在运行上面例子。就不会在陷入死循环了。

volatile是如何保证可见性的?

其他线程又是如何知道共享变量被修改了呢?
为了解决缓存一致性问题,需要遵循一些协议,叫做缓存一致性协议,如:MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。

嗅探

通过嗅探机制来保证及时的知道自己的缓存过期了。
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,
当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里

由于嗅探机制会不断的监听总线,打量使用volatile可能会引起总线风暴

三、原子性

在来看另一种情况。

package com.fzkj.juc;

import java.util.concurrent.TimeUnit;

/**
 * @DESCRIPTION  volatile关键字测试类
 */
public class VolatileTest {

    public static void main(String[] args) {
        atomicity();
    }

    // 原子性
    public static void atomicity(){
        Number num = new Number();
        for (int i = 0; i < 10; i++) { // 启动10个线程
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) { // 每个线程对num的值操作1000次
                    num.add();
                }
            }).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(num.num);
    }
}

class Number{
     int num = 0;

    public void numTo(int target){
        this.num = target;
    }

    public void add(){
        this.num++ ;
    }
}

在上面的例子中,我们启动了10个线程,每个线程调用了1000次add方法,对num的值进行1000累加,那么我们期待的最终结果就是num的值是10000。但是实际上运行程序就会发现,每次的结果都会比10000少。

这个问题的成因,其实跟jvm有关系,我们都知道,程序员写的代码只是给程序员自己看的,还需要将代码编译才是机器执行的。一个++操作被编译成字节码文件之后,可以简化成三个步骤。第一步取值;第二步加一;第三步赋值。所以在高并发的场景下,就会出现值被覆盖的情况。

原子性的定义:指在一组操作中,要么全部操作都成功,要么全部操作都失败。

原子性是JMM的特性之一,但是volatile却并不支持原子性。要想在多线程的环境下保证原子性,可以使用锁机制,或者使用原子类(AtomicInteger)

四、有序性

禁止指令重排就叫做有序性。

什么是指令重排?

为了提高性能,在遵守as-if-serial语义的情况下,编译器和处理器往往会对指令做重排序。在多线程的情况下,指令重排可能会导致一些意想不到的情况。

volatile是怎么禁止指令的重排序的呢?这里又引出一个新的概念:内存屏障

内存屏障

内存屏障的作用是禁止指令重排序和解决内存可见性的问题。

先了解两个指令:

  • store:将缓存中的数据刷新到内存中
  • load:将内存存储的数据拷贝到缓存中

JMM主要将内存屏障分为四类

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2
StoreStore Store1;StoreStore;Store2 确保Store1立刻刷新数据到内存的操作先于Store2
LoadStore Load1;LoadStore;Store2 确保Load1数据装载先于Store2数据刷新
StoreLoad Store1StoreLoad;Load2 确保Store1数据刷新先于Load2数据装载

StoreLoad被称为全能屏障,因其同时具备其他三个屏障的效果,但是相对于其他屏障,消耗会多。

了解了这些,下面就来看看volatile是如何插入内存屏障的。

可以看到,

  • volatile在读操作后面加了LoadLoad和LoadStore屏障
  • 在写操作前后分别加了StoreStore和StoreLoad屏障

这就是说,编译器不会对volatile读和读后面的操作重排序;不会对写和写前面的操纵重排序。这样就保证了volatile本身的有序性。文章来源地址https://www.toymoban.com/news/detail-738802.html

到了这里,关于Java并发编程-volatile的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Java 并发之《深入理解 JVM》关于 volatile 累加示例的思考

    Java 并发之《深入理解 JVM》关于 volatile 累加示例的思考

    在周志明老师的 《深入理解 JVM》一书中关于 volatile 线程安全性有一个示例代码(代码有些许改动,语义一样): 老师的目的是为了说明在多线程环境下 volatile 只能保证可见性而不是线程安全的。但是当在我的 IDEA 下运行时,发现程序是没有输出结果且始终是

    2024年01月20日
    浏览(9)
  • Tomcat 配合虚拟线程,一种新的编程体验

    Tomcat 配合虚拟线程,一种新的编程体验

    Java 21 在今年早些时候的 9 月 19 日就正式发布,并开始正式引入虚拟线程,但是作为 Java 开发生态中老大哥 Spring 并没有立即跟进,而是在等待了两个月后的 11 月 29 日,伴随着 Spring Boot 3.2 版本的发布,在这个版本中也终于是引入了对虚拟线程的支持。 虚拟线程的引入标志着

    2024年02月05日
    浏览(9)
  • java高并发系列 - 第34篇:google提供的一些好用的并发工具类

    java高并发系列第34篇。 环境:jdk1.8。 关于并发方面的,juc已帮我们提供了很多好用的工具,而谷歌在此基础上做了扩展,使并发编程更容易,这些工具放在guava.jar包中。 本文演示几个简单的案例,见一下guava的效果。 需要先了解的一些技术:juc中的线程池、Excecutors、Execu

    2024年02月16日
    浏览(28)
  • Spring是一个开源的Java开发框架,它提供了一种快速、简单的方式来开发企业级应用程序

    Spring是一个开源的Java开发框架,它提供了一种快速、简单的方式来开发企业级应用程序

    Spring是一个开源的Java开发框架,它提供了一种快速、简单的方式来开发企业级应用程序。Spring的主要优点包括简化Java EE开发、提供依赖注入和面向切面编程等功能。以下是Spring的一些核心特性: 依赖注入(DI):Spring通过DI机制,将对象的依赖关系注入到应用程序中,简化了

    2024年02月03日
    浏览(50)
  • Java-并发编程-进阶篇

    Java-并发编程-进阶篇

    在上一篇幅中对并发编程进行了简单介绍:并发与并行,进程与线程,以及并发编程的简单代码 但是在企业中往往并不能解决实际问题,例如: 1.synchronized在企业开发中会大大降低系统的性能,有什么解决方式,或者其他的替代方案 2.当线程被创建并启动以后,它既不

    2024年02月06日
    浏览(14)
  • Java并发编程实战

    2023年06月19日
    浏览(13)
  • 【java】开发——《并发编程》

    【java】开发——《并发编程》

    目录 一.jmm 二.并发了什么 1.只有一个核(单核)并发还有没有意义 2.单核,还有什么可见性问题 3.并发和并行 三.volitaile 1.变量的可见性问题 2.原因是什么 3.本次修改的变量直接刷到主内存 4.声明其他内存对于这个地址的缓存无效 四.happens-befo 1.顺序性问题 五.volitaile+cas 1.原

    2024年02月22日
    浏览(14)
  • Java并发编程:Semaphore

    信号量可以控制线程的并发数量 通常用于那些资源有明确访问数量限制的场景,常用于限流 。 使用 Semaphore 先调用 acquire() 获取,然后通过 try ... finally 保证在 finally 中释放。 Semaphore常用方法说明 acquire() 获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直

    2024年02月12日
    浏览(8)
  • Java并发编程面试题

    Java并发编程面试题

    目录 一、线程、进程、程序 二、线程状态  三、线程的七大参数 四、线程有什么优缺点? 五、start 和 run 方法有什么区别? 六、wait 和 sleep的区别? 七、lock与synchronized的区别 八、Volatile是线程安全的吗?底层原理是什么? 九、synchronized作用和底层原理? 十一、Thre

    2024年02月12日
    浏览(9)
  • Java并发编程详解:实现高效并发应用的关键技术

    在当前的计算机领域,高效的并发编程对于Java开发人员而言变得越发重要。作为流行的编程语言,Java提供了强大的并发编程支持,使开发人员能够充分发挥多核处理器和线程的潜力,构建高性能、高吞吐量的应用程序。本文将深入探讨Java并发编程的关键技术,包括线程安全

    2024年02月13日
    浏览(17)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包