关于volatile使用问题

目录

前言

一、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算法来配合使用实现高效的线程安全,究其根本原因还是因为开发人员需要在线程并发与安全之间达到一个更好的平衡而被设计出来的一个机制。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值