关于java:聊聊volatile

一、作用

1、保障线程可见性

main线程和T线程共享堆内存的数据;main和T也有本人的工作空间,当要拜访共享内存的数据flag,会把共享内存的flag复制一份到本人的工作空间中。

例如main线程对flag进行了扭转,首先是在本人的空间进行扭转,批改后的值会马上批改到共享内存;然而T线程何时查看共享内存的值有没有被扭转不好管制。

即 main线程的批改并没有及时的反馈到T线程,也就是线程之间不可见。对这个变量加了volatile后,可能保障一个线程对这个变量批改后,另一个线程可能马上晓得。

底层是通过CPU的缓存一致性协定(MESI)保障的

2、禁止指令重排序

指令重排序:CPU原来执行一条指令时,是一步一步的程序执行;当初的CPU为了提高效率,会并发的执行指令,就是第一个指令执行到一半时,第二个指令可能曾经开始执行了,也就是流水线似的执行。这时就要求编译器,要可能对指令重排序产生影响的状况进行相应的解决,此时volatile就派上用场了。

DCL单例(Double Check Lock)来解释

/**
 * @author Java和算法学习:周一
 */
public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            //双重查看
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Singleton.getInstance().hashCode());
            }).start();
        }
    }
    
}

INSTANCE = new Singleton();(如果外面有个变量int a = 100)new对象的过程分为3步
1)申请内存(赋默认值);a=0
2)初始化;a=100
3)赋值,把值给变量;把a的值赋值给INSTANCE,即让INSTANCE指向变量a的地址

如果外面存在指令重排序的话,就存在还未初始化的变量就进行了赋值操作,即2、3两步地位替换了;也就是对象处于半初始化状态就进行了赋值操作。当第一个线程(只管加了锁),执行到new Singleton()时,new了一半;此时第二个线程来了,首先判断INSTANCE 是否为空,因为INSTANCE曾经是半初始化状态,外面曾经有值了,不再是空值了,也就是第二个线程曾经拿到了这个对象了,这个线程就能够间接应用该对象了,很可能就会应用外面的这个值。原本冀望这个值是100,然而这个值却是0,如果这个值是订单数的值这时就存在问题。加了volatile后,对这个对象的指令重排序就不容许存在,即必须是在初始化实现之后才会进行赋值操作

3、volatile不能保障原子性

/**
 * @author Java和算法学习:周一
 */
public class T {
    public volatile int count = 0;

    public synchronized void m() {
        for (int i = 0; i < 1000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threadList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threadList.add(new Thread(t::m));
        }

        threadList.forEach((o)->{
            o.start();
        });
        threadList.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }

}

m办法,如果不加synchronized,count始终不会到10000。起因是,当一个线程把count值批改为1后,此时进来了第二、第三个线程,都读到count为1,批改后把count加1(即2)写回去,这时两个线程对count批改后,count的值只从1变到了2,所以最初的后果总是小于10000。归根结底就是count的值是保障了可见性,然而count++自身不是原子性的操作(底层分为几个步骤)。

volatile能保障线程的可见性,然而并不能代替synchronized保障原子性。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理