2

Java多线程是一个庞大的知识体系,这里对其中的volatile进行一个总结,理清他的来龙去脉。

CPU缓存

要搞懂volatile,首先得了解CPU在运行过程中的存储是如何处理的,其结构如图

clipboard.png

CPU会把一些经常使用的数据缓存在cache中,避免每次都去访问较慢的memory。在单线程环境下,如果一个变量的修改都在cache中,自然不会有什么问题,可是在多线程环境中就可能是下面这个图的示意图(单核另当别论)

clipboard.png

CPU1 修改了一个变量a存入cache1,但是CPU2 在cache2中看到的a任然是之前的a,所以造成CPU1修改失效,我们来看看示例代码:

import java.util.concurrent.TimeUnit;

public class Counter {
    private static  boolean stop ;
    //private static volatile boolean stop ;
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stop) {
                    i++;
                }
            }
        } );
        t.start();

        TimeUnit.MILLISECONDS .sleep(5);
        stop = true;
    }
}

在我的4核笔记本上运行结果:

clipboard.png

就一直运行着,没有停止(需要手工停止),这说明在主线程中修改的stop变量后,线程t没有读取到最新的stop的值,还一直是false。

volatile原理

volatile的原理就是,如果CPU1修改了一个变量a,不仅要修改自身的cache,还要同步到memory中去,并且使CPU2的cache中的变量a失效,如果CPU2要读取a,那么就必须到memory中去读取,这样就保证了不同的线程之间对于a的可见性,亦即,无论哪个线程,随时都能获得变量a最新的最新值。
我们来看看示例代码:

import java.util.concurrent.TimeUnit;

public class Counter {
    //private static  boolean stop ;
    private static volatile boolean stop ;
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stop) {
                    i++;
                }
            }
        } );
        t.start();

        TimeUnit.MILLISECONDS .sleep(5);
        stop = true;
    }
}

在我的4核笔记本上运行结果:

clipboard.png

很快程序就结束了,说明线程t读到了经主线程修改后的stop变量,然后就停止了。

(例子源于《effective Java》)

volatile使用场景

状态标志

就像上面的代码里,把简单地volatile变量作为状态标志,来达成线程之间通讯的目的,省去了用synchronized还要wait,notify或者interrupt的编码麻烦。

替换重量级锁

在Java中synchronized 又称为重量级锁,能够保重JMM的几大特性:一致性,原子性,可见性。但是由于使用了锁操作,在一定程度上会有更高的性能消耗(锁的线程互斥性亦即资源消耗)。而volatile能提供可见性,原子性(单个变量操作,不是a++这种符合操作),所以在读写上,可以用volatile来替换synchronized的读操作,而写操作仍然有synchronized实现,能取得更好的性能。

import java.util.ArrayList;
import java.util.List;

public class Counter1 {

    private class Count11 {
        private  int value;
        public synchronized int getValue() {
            return value;
        }
        public synchronized int increment() {
            return value++;
        }
    }

//    private class Count11 {
//        private volatile int value=0;
//        int getValue() {  return value;    }
//        synchronized int increment() {    return value++;    }
//    }

    public static void main(String[] args) throws Exception {
        Counter1.Count11 count11 = new Counter1().new Count11();
        List<Thread> threadArrayList = new ArrayList<>();
        final int[] a = {0};
        Long allTime = 0l;
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 4; i++) {
            Thread t = new Thread(() -> {
                int b = 0;
                for (int j = 0; j < 10000; j++) {
                    count11.increment();
                    a[0] = count11.getValue();
                }
                for (int j = 0; j < 10000; j++) {
                    b++;
                    a[0] = count11.getValue();
                }
            });
            t.start();
            threadArrayList.add(t);
        }
        for (Thread t : threadArrayList) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        allTime = ((endTime - startTime));
        System.out.println("result: " + a[0] + ", average time: " + (allTime) + "ms");



    }

}

volatile优化结果:

result: 40000, average time: 124ms
result: 40000, average time: 133ms
result: 40000, average time: 141ms
result: 40000, average time: 112ms
result: 40000, average time: 123ms
result: 40000, average time: 143ms
result: 40000, average time: 120ms
result: 40000, average time: 120ms

未优化结果:

result: 40000, average time: 144ms
result: 40000, average time: 150ms
result: 40000, average time: 149ms
result: 40000, average time: 165ms
result: 40000, average time: 134ms
result: 40000, average time: 132ms
result: 40000, average time: 157ms
result: 40000, average time: 138ms
result: 40000, average time: 158ms

可见使用volatile过后效果的确优于只使用synchronized的性能,不过试验中发现有个阈值,如果读取修改次数较小,比如1000以内,只使用synchronized效果略好,存取次数变大以后 volatile的优势才慢慢体现出来(次数达到10000的话,差距就在60ms左右)。

待挖掘

还有很多用法,在将来的学习中,不断总结与挖掘。

联想

无论处于应用的哪一层,优化的思路都是可以相互借鉴的,比如我们做一个服务集群,如果每一个节点都要保存所有用户的session,就很难使得session同步,我们就可以借鉴volatile这种思路,在集群之上搞一个调度器,如果某一个节点修改了一个用户session,就报告给调度器,然后调度器通知其他所有节点修改该用户session。而一般情况下,数据的读写比都比较高,所以这样做就能到达一个很好的性能。

注意事项

引用类型的volatile只在引用本身发生变化时具有可见性,其引用的对象的元素发生变化时不具有可见性

欢迎访问我的个人主页 mageek(mageek.cn)


MageekChiu
4.4k 声望1.7k 粉丝

T