面试精选

volatile关键字——最轻量的同步

2021-03-18  本文已影响0人  奔跑吧李博

我们都知道,现在不管是手机还是电脑,动不动就声称是多核的,多核就是多CPU的意思。因为一个CPU在同一时间其实只能处理一个任务,即使我们开了多个线程,对于CPU而言,它只能先处理这个线程中的一些任务,然后暂停下来转去处理另外一个线程中的任务,以此交替。而多CPU的话,则可以允许在同一时间处理多个任务,这样效率当然就更高了。

随着CPU读取速度越来越快,就不再是每次去从内存中读取数据,CPU厂商引入了高速缓存功能。内存里存储的数据,CPU高速缓存里也可以存一份,这样当频繁需要去访问某个数据时就不需要重复从内存中去获取了,CPU高速缓存里有,那么直接拿缓存中的数据即可,这样就可以大大提升CPU的工作效率。

场景举例:

有两个线程,分别通过两个CPU来执行程序,但它们是共享同一个内存的。现在CPU1从内存中读取数据A,并写入高速缓存,CPU2也从内存中读取数据A,并写入高速缓存。仅读取的情况下是没有问题的。

但是如果线程2修改了数据A的值,首先CPU2会更新高速缓存中A的值,然后再将它写回到内存当中。这个时候,线程1再访问数据A,CPU1发现高速缓存当中有A的值啊,那么直接返回缓存中的值不就行了。此时线程1和线程2访问同一个数据A,得到的值却不一样了。

而此时volatile就来出面解决这个问题了。

可见性

Java提供了volatile关键字来保证可见性。

当一个变量被声明成volatile之后,任何一个线程对它进行修改,都会让所有其他CPU高速缓存中的值过期,这样其他线程就必须去内存中重新获取最新的值,也就解决了可见性的问题。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

示例:

public class VolatileCase {
    private static boolean ready;
//    private volatile static boolean ready;
    private static int number;
    
    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready);//无限循环
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        SleepTools.second(1);
        number = 51;
        ready = true;
        SleepTools.second(5);
        System.out.println("main is ended!");
    }
}

该示例中,在使用valotile声明ready之前。开启子线程,在主线程改变ready值,但是while的无限循环不会停下来,子线程中得不到在主线程中被修改的值。加了volatile修饰后,主线程修改了之后,子线程使用就能立马获取到ready的真实值了,这就是valotile的可见性。

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

原子性

volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性。volatile可以修饰变量,用于多线程的访问,但是在多核的情况下,多线程并发还是存在问题。

原子性验证示例:

public class NotSafe {
    private volatile long count =0;

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    //count进行累加
    public void incCount(){
        count++;
    }

    //线程
    private static class Count extends Thread{

        private NotSafe simplOper;

        public Count(NotSafe simplOper) {
            this.simplOper = simplOper;
        }

        @Override
        public void run() {
            for(int i=0;i<10000;i++){
                simplOper.incCount();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NotSafe simplOper = new NotSafe();
        //启动两个线程
        Count count1 = new Count(simplOper);
        Count count2 = new Count(simplOper);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(simplOper.count);//20000?
    }
}

用volatile对count修饰,两个线程同时对count累加,其结果却是每次基本都不一样。

那么总结来了

volatile关键字适用于什么样的场景了?适用于一写多读的场景,即只有一个线程来修改值,其他线程来读取值。而多个线程都会写,那么就得用上sychronized关键字了。

参考:

volatile关键字在Android中到底有什么用?

上一篇下一篇

猜你喜欢

热点阅读