关于volatile使用问题

为什么不使用volatile,其它线程也能得到当前线程修改后的值,不使用volatile也不存在可见性问题?原来解决可见性问题不一定需要volatile,println也可以 1. 可见性 可见性指多线程编程下,其中一个线程修改了变量的值,其它的线程能得到修改后的值。 2. 为什么不使用volatile,其它线程也能得到变量修改后的值 实验代码分析: ① 初始变量a=0,b=false; ②然后,一个线程判断b 的值是否为false,是false那么循环输出“b is false and a is ”信息,直到 b 的值变为true,输出“b is true and a is”信息结束线程; ③然后延迟2ms,为了让后面的线程晚于前面线程执行; ④最后,另一.. 阅读详情

目录

前言

一、volatile是什么?

二、volatile的可见性

三、volatile的有序性

四、volatile的使用

五、关于volatile的一个小问题

总结


前言

在java的多线程环境下,往往会因为需要提高代码效率而导致线程不安全,于是java开发人员为了实现既能够高效开发又可以保证数据的不丢失或者是错误数据而设计了一系列的锁,包括synchronized,lock,reentrantlock等,同时不同锁的颗粒度大小也不同,颗粒度越细的锁对于cpu的利用也越高。在这样的需求之下,java开发人员设计出了volatile。


提示:以下是本篇文章正文内容,下面案例可供参考

一、volatile是什么?

        volatile是一个用于修饰变量的轻量级修饰符,它相当于锁,更像是一种同步机制,它可以实现代码的可见性和有序性,但是不保证代码的原子性。

二、volatile的可见性

        关于实现可见性,volatile主要是通过添加Lock前缀指令以及MESI缓存一致性协议的方式来实现的,对于写操作,JVM会发送一个lock指令给CPU,这可以使总线得到lock锁,从而阻塞其他CPU对内存的访问,在CPU在执行完写操作后,会立即将值刷新到内存中,同时因为MESI缓存一致性协议,其他各个CPU都会对总线有感知,查看自己本地缓存中的数据是否有被修改,如果发现修改,则会将本地缓存的数据过期掉,这样每次读取加载的就是内存中的最新值了。

三、volatile的有序性

        关于实现有序性,volatile主要是通过添加内存屏障的方式,阻止了指令的重排序,关于内存屏障,主要有四个类型,分别是

屏障类型       使用场景说明
LoadLoadL1;LoadLoad;L2L1,L2代表两条读指令,在L2要读取的数据被访问前,必须确保L1的数据被读取完毕
StoreStoreW1;StoreStore;W2W1,W2代表两条写指令,在W2写入操作执行前,必须确保W1的写入操作对其他处理器可见(刷到内存)
LoadStoreL1;LoadStore;W2L1表示读指令,W2表示写指令,必须确保L1的读指令操作在W2写入操作执行之前
StoreLoadW1;StoreLoad;L2W1表示写指令,L2表示读指令,必须确保W1在对其他处理器可见的情况下,L2未被执行,该内存屏障消耗资源最大

而在volatile中,主要是确保了其读,写操作的有序性,即

  • 在每个读操作之前插入一个LoadLoad屏障,保证该变量读操作的时候,如果被其他线程修改过,一定会从其他cpu的主存加载到本地,保证其读取到的值是有序的,然后在读操作之后插入一个LoadStore屏障,禁止读操作发生在后面的其他写操作之后。
  • 在每个写操作之前插入一个StoreStore屏障,保证该变量写操作不会发生在别的写操作之前导致更新丢失,在写操作之后插入一个StoreLoad屏障,保证数据一定是在被刷到内存以后才可以被读取,保证读取的值为最新值。

当然,JMM也具备一些先天的有序性,这些有序性不需要经过任何手段就可以保证有序,它们被称为happens - before原则,一共有八条,分别是:

  • 程序顺序原则:一个线程中的每个操作,happens-before于该线程的任意后续操作
  • 监视器锁原则:对一个线程的解锁,happens-before于该线程的加锁
  • volatile原则:对一个volatile域的写,happens-before于对这个volatile域的读
  • 传递性原则:如果A happens-before B ,且B happens-before C ,那么A happens-before C
  • start()原则:如果线程A执行ThreadBstart(),那么A线程的ThreadBstart() happens-before于B中的任意操作
  • join()原则:如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • interrupt()原则:对于线程interrupt()方法的调用先行发生于被中断线程代码检测到终端时间的发生,可以通过Thread.interrupted()方法检测是否有中断发生
  • finalize()原则:一个对象的初始化happens-before于它的finalize()方法的开始

四、volatile的使用

        从volatile的特性可以看到,volatile有着有序性和可见性两大特性,但是为了实现线程安全,还需要实现原子性,所以对于volatile所修饰的变量,要么就是让引用其变量的代码为具有原子性的代码,要么就是在对应代码块通过加锁的方式实现原子性,第三种就是通过CAS的方式实现变量修改原子性。

        其实现场景有双检锁单例模式的修饰私有对象,以及ConcurrentHashMap的修饰数据结构。

具体代码举例:

  双检锁模式实现

public class Singleton implements Serializable {
        //以懒加载为最初实现设计,通过volatile实现变量在代码编写过程中的原子性
        private static volatile Singleton singleton = null;  

        private Singleton(){}

        private static Singleton getInstance(){

            if (singleton == null){
                //通过synchronized加锁的方式,保证类只被加载一次
                synchronized (Singleton.class){
                    if (singleton == null){
                        singleton = new Singleton();
                    }

                }

            }
            return singleton;
        }
}

如果在上述代码中,没有使用volatile修饰变量的话,也许会出现这样一种场景,有两个线程同时调用了额Singleton类,然后线程1的new代码进行了重排序,这样子的话可能最终没有创建完对象,线程就已经释放了锁,此时线程2开始加载,这样就会导致创建了两个对象。

五、关于volatile的一个小问题

以下有一个实现int自加的方法,但是他最终输出的结果却不是期望值

class test3 {
    static volatile int a = 0;  //利用volatile修饰变量
    
    static void add() {
        a++;
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                        add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){    //活跃线程数量大于2
            Thread.yield();                //线程让队
        }
        System.out.println(a);             //输出结果
    }
}

此时随机取几个输出值

 

 可以发现,输出值并不总是10000,中途有出现过数据丢失的情况,这正是因为volatile它并不保证原子性的原因,那么为了实现最终输出结果都是期望值,有两种做法,一个是加sychronized锁在线程中,一个就是采用CAS形式的原子性自加方法。

方案1:使用锁

class test3 {
    static volatile int a = 0;

    static void add() {
        a++;
    }

    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                synchronized (lock) {        //给每一个线程安排一个锁
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(a);
    }
}

方案2:使用CAS方法

import java.util.concurrent.atomic.AtomicInteger;

class test3 {
    static AtomicInteger atomicInteger;     //定义一个全局范围的atomicInteger


    public static void main(String[] args) throws Exception {
        atomicInteger = new AtomicInteger(0);   //设初值为0
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        //相当于先获得值再自加
                        atomicInteger.getAndIncrement();
                        //相当于先自加再获得值
     //                 atomicInteger.incrementAndGet();
                    }
            }).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(atomicInteger.get());   //输出值
    }
}

稍稍深入了解一下这个AtomicInteger。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

这个AtomicInteger类中定义了两个属性,分别是unsafe和valueOffset,其中unsafe就是Unsafe类的实例,而valueOffset就是这个对象的某一属性的偏移量,每个属性的偏移量都不同,所以Unsafe也就能够通过对象的实例和对象中属性的偏移量来获得对象对应属性在内存中的值了。

而AtomicInteger中的自加方法,就是调用了Unsafe类下的CAS方法

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

其中的getAndAddInt方法就是通过循环判断内存中的值来进行校验,实现数据的原子性

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

题外话:在代码编写过程中,可能会发现有那么几行代码很奇怪,他们和整个代码的逻辑性似乎没有关系,但是为什么需要加呢,这几行代码就是

 while (Thread.activeCount()>2){
            Thread.yield();
        }

那么把他们删除看一下

class test3 {
    static volatile int a = 0;

    static void add() {
        a++;
    }

    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                synchronized (lock) {        //给每一个线程安排一个锁
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }
            }).start();
        }

        System.out.println(a);
    }
}

 可以发现,输出的值不是之前的10000了,难道说即使加了锁还是会导致数据丢失吗。

为了解开这个困惑,我们需要在代码中加一行

 Thread.currentThread().getThreadGroup().list();

通过在控制台输出当前活跃的线程来进行观察

从控制台中的结果中可以看出,结果输出语句在所有线程执行完之前就已经被执行了,虽然此时main函数还没有结束,但是输出代码已经运行完成了,这就导致了数据丢失的错觉。

所以需要增加一个线程让步在输出语句上方,保证输出语句在最后输出。

那么既然需要让步,那么为什么判断的活跃线程数是2而不是1呢,毕竟只有一个main函数是一直存在的线程,这个也可以通过上述代码来进行查看。

  while (Thread.activeCount()>2){
            Thread.yield();
            Thread.currentThread().getThreadGroup().list();
        }

在最后的输出结果中发现,事实上一直有两个线程没有被销毁,一个是main函数对应的线程,而另一个则是守护线程。守护线程会直到非守护线程全部销毁后再销毁,这样也就知道了如果设置的判断条件是Thread.activeCount()>1,那么这就成为一个死循环了,永远也不能抵达输出结果的真实。 


总结

        其实说来说去volatile就是一个比较轻量级的同步机制,它相当于synchronized粒度更细,可以和锁或者是CAS算法来配合使用实现高效的线程安全,究其根本原因还是因为开发人员需要在线程并发与安全之间达到一个更好的平衡而被设计出来的一个机制。

Ubuntu断网环境下的系统更新:apt-offline保姆级教程(附常见问题排查) 本文详细介绍了在Ubuntu断网环境下使用apt-offline进行系统更新的完整教程,包括环境准备、签名文件生成、数据传输方案对比及常见问题排查。特别适用于企业级开发环境或安全敏感场景,帮助运维人员高效完成离线系统更新,确保系统安全与稳定。 阅读详情

相关推荐

mapbox高阶,加载倾斜摄影

本文详细介绍如何基于mapbox-gl v3.8.0版本实现倾斜摄影的加载,亲测可用。希望能帮助到您。一起学习,加油!加油!

我哎GIS博客 302

volatile问题汇总

volatile作用 可见性:所有线程都能看到共享内存的最新状态,不保证操作的原子性。 防止指令重排。 volatile原理 从Java内存模型理解,volatile要求d读操作(read、load、use),写(assign、store、write)动作必须连续出现,从而保证读操作会先刷新工作内存,写操作会立即同步到主内存。 通过内存屏障控制CPU的store buffer和invalid queue的行为来防止指令重排。 Store buffer 和Invalid Queue CPU修改Cac

Xp545945的博客 455

IEC 61000-4-2-2025-168页.rar

IEC 61000-4-2-2025-168页.rar

【并发编程】(七)volatile原理——解决可见性、有序性问题

volatile的作用是并发环境下,在一定的作用范围内解决共享变量的可见性和有序性问题,相对于synchronized和显示的加锁,volatile在性能上根据优势,可以尽可能的以更细的粒度来保证线程安全。通过JMM提供的lock指令,来使用CPU底层的提供的总线锁或缓存锁来保证共享资源的实时同步和更新。总线锁在加锁后会阻塞CPU访问内存,所以大多数CPU是通过缓存一致性协议对缓存行状态的控制来达到多个线程之间的共享变量值同步。JMM提供了四种内存屏障,插入到各个指令之间,让。

积木成林,聚沙成塔 5566

volatile关键字的使用

是 C/C++/Java 等语言中的一个类型修饰符,:变量的值可能由硬件(如外设状态寄存器)自动更新,编译器不能假设它不变。:主程序和中断都会访问该变量,防止编译器优化掉看似多余的读取。:一个线程修改,另一个线程读取,避免因优化导致读取不到最新值。

2402_88673453的博客 193

Java|多线程与高并发】volatile关键字和内存可见性问题

volatile都是Java多线程中很重要的关键字,但它们的作用和使用场景有所不同。synchronized关键字可以保证同一时刻只有一个线程可以访问被synchronized关键字保护的代码块,从而避免多个线程对共享资源的并发访问导致的数据不一致问题。关于关键字更详细的介绍,可以参考我之前写的这篇文章线程安全问题以及synchronized使用实例volatile用于保证变量在多个线程之间的可见性和有序性。本文主要介绍valatite关键字在介绍volatile关键字之前,

云泽野の编程日记 1425

volatile 用法以及大家遇到的问题

一:Volatile的定义: 被volatile所修饰的变量,编译器在读取这个变量的值时就不会进行优化。因为编译器会认为这个变量是“易变”的,所以直接访问该变量的原始地址而不是寄存器。 有人会问什么是编译器优化,听我细细道来: 所谓的编译器优化是指在没有volatile修饰的变量,编译器会认为该变量不会被其他程序或者硬件修改,所以编译器会将变量缓存到寄存器避免访问内存,因为访问寄存器的速...

huobengluantiao8 320

volatile的详细解释以及应用场景

volatile的详细解释以及应用场景

cloud_fly的博客 869

volatile到底解决了什么问题

本文面向的读者是对java熟悉,并对volatile有一定的了解的java programer。(volatile简介:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html 建议先看前几段了解下即可。不看happens-before等java理论)市面上对该关键字的解读,包括《并发编程的艺术》中都对最底...

weixin_30735745的博客 650

volatile解决什么问题

了解java内存模型,每个线程都有自己的工作内存,工作内存都会从主内存拷贝一份变量(本质上就是高速缓存,这样设计就是为了更好的运行速度),上述循环一致读的是工作内存中的变量。这就是可见性问题,voliatile就是解决此问题的。2. 但是操作boolean,以及赋值操作,天然是原子操作。1. volatile 保证了可见、有序,但是不能保证原子。测试线程安全问题,下面代码,main线程会一直死循环,不会进入判断break。volatile: 保证每次使用变量,都是先从主内存上刷新到本地内存。

HeiShuaiGuo的博客 186

volatile使用以及引起的问题解决方法

volatile使用以及引起的问题解决方法

jcoiwenwfkowe的博客 197

Java关于volatile的一些问题

文章目录1. 开始2. 并发编程中的三个重要概念3. 可见性4. 有序性5. volatitle不保证原子性 1. 开始 仔细说volatile是一个复杂的问题,可以从Java内存模型聊到缓存一致性协议,很难界定学到什么地方为止。 很多时候,我们并不需要那么复杂,我们需要更加实用。 所以,下面我们就来聊聊volatitle在实际开发中的问题。 2. 并发编程中的三个重要概念 原子性:是指一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行 可见性:是指当多个线程访问同一个变量

trayvontang的博客 382

Volatile、CAS、原子类问题

Volatile、CAS、原子类问题

weixin_51360664的博客 232

多线程编程6——使用 volatile 解决问题可见性问题

什么是内存可见性问题?如何解决内存可见性问题?(使用 volatile关键字)synchronized 和volatile的区别

Fighting!!! 1169

volatile说到i++的线程安全问题

程序员的成长之路互联网/程序员/技术/资料共享关注阅读本文大概需要 4 分钟。作者:ZimZzcnblogs.com/zemliu/p/3298685.html简介volatile关键...

良月柒 544

一篇文章吃透volatile常见面试问题,可见性、JMM、指令重排等。

关键字用于修饰变量,它的作用是保证变量的可见性和禁止指令重排序。当一个变量被声明为 volatile 时,它的值会被立即写入主内存,并且每次读取该变量时都会从主内存中读取,而不是从缓存中读取。这样可以确保不同线程对该变量的操作是可见的,并且避免了指令重排序可能导致的问题。​可见性问题是指在软件开发过程中,由于多个线程或进程同时访问共享资源,导致数据不一致或错误的情况。例如,一个线程修改了共享变量的值,而另一个线程在不知情的情况下读取了该变量的值,从而导致程序出现错误。

4G/5G随身WiFi专业DIY改装 514

volatile有什么用?怎么用?是什么?

volatile volatile修饰的变量,其进行读写操作时都是从主存访问,而不是Cache volatile解决了什么问题 一、变量的可见性问题 在多线程应用中,如果变量没有被volatile修饰,那么每个线程可能从主存中复制变量到CPU的缓存里。而且如果你的电脑是多核处理器,那么有可能会出现每个单核中的线程都复制一次变量。可以通过图来理解下: 也就是说,对于没有被volatile修饰的变量,虚拟机无法保证线程数据是读取从主存读取到cache,写入操作是从cache到主存。那么,这会带来什么问题呢?

浮生小二的博客 644

PC寄存器

连接:https://docs.oracle.com/javase/specs/jvms/se8/html/ 寄存器存储指令相关的线程信息,CPU只有把数据装载到寄存器才能运行。 JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。 作用:线程独有,用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令! 介绍: 他是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。

weixin_45650737的博客 600

volatile分析

volatile分析 解决问题 问题:多线程编程,共享变量需要多个线程可见,需要保证线程的执行顺序 解决: 禁止编译器优化而重排序指令。 对 volatile 修饰的变量值,保证线程读取到的值是最新的,而不是寄存器中缓存的值。 优势劣势 优势 轻量级锁 劣势 不保证原子性 适用场景 适用条件 对变量的写操作不依赖于当前值。 该变量没有包含在具有其他变量的不变式中。 实例 开销较...

nmgyangguangyuan的博客 189

Java线程】volatile的适用场景

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子性。这就是说线程能够自动发现 volatile 变量的最新值。 Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。 出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而

Alpha's 学习笔记 4万+
上一篇: vue-cli 前端与 spring后端项目部署流程
下一篇: Vscode提示拒绝访问
niteqaq
博客等级 码龄6年 11粉丝 6原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值