java面试精选

volatile关键字

2021-03-10  本文已影响0人  因你而在_caiyq

原创文章,转载请注明原文章地址,谢谢!

volatile关键字理解

volatile是Java提供的轻量级的同步机制,其主要有三个特性

保证内存可见性
当某个线程在自己的工作内存中将主内存中共享数据的副本,修改并刷新到主内存后,其它线程能够立即感知到该共享数据发生变化。
public class VolatileDemo {

    private int num;
    //private volatile int num;

    private void add() {
        num = 10;
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            demo.add();
            System.out.println("num值发生变化");
        }).start();
        while (demo.num == 0) {
        }
        System.out.println("main线程感知到了num值发生变化");
    }
}
运行结果

程序一直处于运行中,并未停止,这是因为程序一直处于while循环中,main线程并未感知到num值的变化。而当num使用volatile修饰的时候,程序将正常执行运行结束,因为线程在修改num值的时候,这时候对main线程是可见的,这样就会跳出while循环,结束。

不保证原子性

不保证原子性正是volatile轻量级的体现,多个线程对volatile修饰的变量进行操作时,会出现容易出现写覆盖的情况。

public class VolatileDemo {

    private volatile int num;

    private void add() {
        num++;
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                demo.add();
            }).start();
        }
        System.out.println("main线程执行结束,num值为:" + demo.num);
    }
}
运行结果

100个线程分别执行num++操作,理论上结果应该是100,但是实际结果是小于100,这是因为num++不是原子操作,volatile不保证原子性。解决方法是使用java.util.concurrent.atomic包下的原子类AtomicInteger。

public class VolatileDemo {
    private static AtomicInteger num = new AtomicInteger(0);

    private static void add() {
        num.incrementAndGet();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                add();
            }).start();
        }
        while (100 != num.get()) {
        }
        System.out.println("main线程执行结束,num值为:" + num);
    }
}
禁止指令重排序
计算机执行程序是为了提高性能,编译器和处理器常常会进行指令重排。单线程环境下程序最终执行结果和执行顺序一致,多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程使用的变量一致性无法保证。处理器在进行指令重排的时候必须考虑指令之间的数据依赖性。

volatile实现禁止指令重排的原理

volatile的实际应用
单例模式为什么需要volatile关键字修饰?
public class SingletonDemo {
    private static volatile SingletonDemo singletonDemo;
    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if (singletonDemo == null) {                     //1
            synchronized (SingletonDemo.class) {         //2
                if (singletonDemo == null) {             //3
                    singletonDemo = new SingletonDemo(); //4
                }
            }
        }
        return singletonDemo;
    }
}

以上单例模式使用的是双检索方式,且是懒汉模式。先判断singleton是否已经初始化,如果初始化了就直接返回,如果没有初始化,则创建对象。

在多线程情况下,只有1,没有2、3,就可能导致创建多个实例。比如,线程A和线程B都调用getInstance方法,线程A判断了代码1,然后切换到了线程B,线程B判断代码1,然后创建了singleton实例,然后切换到了线程A,此时线程A又创建了singleton实例,这样就创建了多个singleton实例。

加入synchronized保证同一时刻只有一个线程进入临界区。首先假设没有代码3,考虑这样的场景。此时假设线程A和线程B都判断了代码1,进入代码2,线程A先进入临界区,线程B发现线程A在临界区,便在队列中等待。线程A继续执行,创建了一个singleton实例,退出临界区。然后线程B进入临界区,又创建了singleton实例,结果又是两个singleton实例。所以代码3的作用是必须的。

如果代码2和代码3都存在,那么当线程A创建了singleton实例后,线程B判断singleton不为null,就不会再创建实例了。这样一来的话,实际上代码1和代码3的作用似乎一样。但是考虑这样的一个场景,假设没有代码1,通过线程A和线程B,singleton已经创建好了,此时来了个线程C,直接进入临界区加锁,然后判断singleton实例不为null,跳出。但是这样一上来就加锁,是较消耗资源的,所以代码1的作用就不言而喻了。

最后singleton变量为什么要用volatile修饰呢?分析一下new一个对象,需要有几个步骤

上述步骤中,cpu为了优化程序,可能会进行指令重排序,打乱第3、4步骤,导致实例还没创建,引用指向的变量就被使用了。比如,线程A执行到new Singleton(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。这时时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量。加了volatile之后,就保证new不会被指令重排序。

博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!

上一篇下一篇

猜你喜欢

热点阅读