首页 > 代码库 > 再谈Java原子变量以及同步的效率 -- 颠覆你的人生观

再谈Java原子变量以及同步的效率 -- 颠覆你的人生观

思维定视让我们觉得原子变量总是快过同步操作的,笔者也是一直这么认为,直到一次实现一个ID生成器的过程中的一次测试偶然发现了并非都这么回事。


测试代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrentAdder {
    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
    private static int I = 0;
    private static final Object o = new Object();
    private static volatile long start;

    public static void main(final String[] args) {
        //每个线程执行多少次累加
        int round = 10000000;
        //线程个个数
        int threadn = 20;
        start = System.currentTimeMillis();
        atomicAdder(threadn, round);
        //syncAdder(threadn, round);
    }


    static void atomicAdder(int threadn, int addTimes) {
        int stop = threadn * addTimes;
        List<Thread> list = new ArrayList<Thread>();
        for (int i = 0; i < threadn; i++) {
            list.add(startAtomic(addTimes, stop));
        }
        for (Thread each : list) {
            each.start();
        }

    }

    static Thread startAtomic(final int addTimes, final int stop) {
        Thread ret = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < addTimes; i++) {
                    int v = ATOMIC_INTEGER.incrementAndGet();
                    if (stop == v) {
                        System.out.println("value:" + v);
                        System.out.println("elapsed(ms):" + (System.currentTimeMillis() - start));
                        System.exit(1);
                    }
                }
            }
        });
        ret.setDaemon(false);
        return ret;
    }



    static void syncAdder(int threadn, int addTimes) {
        int stop = threadn * addTimes;
        List<Thread> list = new ArrayList<Thread>();
        for (int i = 0; i < threadn; i++) {
            list.add(startSync(addTimes, stop));
        }
        for (Thread each : list) {
            each.start();
        }

    }


    static Thread startSync(final int addTimes, final int stop) {
        Thread ret = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < addTimes; i++) {
                    synchronized (o) {
                        I++;
                        if (stop == I) {
                            System.out.println("value:" + I);
                            System.out
                                .println("elapsed(ms):" + (System.currentTimeMillis() - start));
                            System.exit(1);
                        }
                    }
                }
            }
        });
        ret.setDaemon(false);
        return ret;
    }
}


这是一个很简单的累加器,N个线程并发累加,每个线程累加R次。


分别注释

atomicAdder(threadn, round);//原子变量累加
syncAdder(threadn, round);//同步累加
中的一行执行另一行


笔者机器的配置:i5-2520M 2.5G 四核

N=20

R=10000000

结果:

原子累加:15344 ms

同步累加:10647 ms


问题出来了,为什么同步累加会比原子累加要快50%左右?



@ 我们知道java加锁的过程是(内置sync和显式lock类似),要加锁的线程检查下锁是否被占用。如果被占用则加入到目标锁的等待队列。如果没有则,加锁。


这里我们每个线程获取到锁累加之后就立马又去获取锁,这时其他线程还没有被唤醒,锁又被当前线程拿到了。这也就是非公平锁可能造成的饥饿问题。


但是这一个原因不能解释50%的性能提升?理论上,在一个绝对时间,总有一个线程累加成功,那么两种累加器的耗时应该近似才对。


那么是有什么提升了同步累加的性能,或者是什么降低了原子累加的性能?


@接下来笔者分别perf了一下两种累加器的执行过程:

第一次执行的是原子累加器,第二次执行的同步累加器。


wxf@pc:/data$ perf stat -e cs -e L1-dcache-load-misses java ConcurrentAdder
value:100000000
elapsed(ms):8580

 Performance counter stats for 'java ConcurrentAdder 1 100 1000000':

       21,841 cs                                                          
       233,140,754 L1-dcache-load-misses                                       
       8.633037253 seconds time elapsed
wxf@pc:/data$ perf stat -e cs -e L1-dcache-load-misses java ConcurrentAdder
value:100000000
elapsed(ms):5749

 Performance counter stats for 'java ConcurrentAdder 2 100 1000000':

       55,522 cs                                                          
       28,160,673 L1-dcache-load-misses                                       
       5.811499179 seconds time elapsed


我们可以看出,同步累加的上下文切换是要比原子累加多,这个可以理解,加锁本身就会增加线程的切换。

再看,原子累加器的L1缓存失效比同步累加器高一个数量级


笔者茅塞顿开,原子操作会导致缓存一致性问题,从而导致频繁的缓存行失效。缓存一致性协议MESI见:http://en.wikipedia.org/wiki/MESI_protocol

但是这时同步累加器在一个CPU周期内反复的获取锁操作,缓存并没有失效。

再把每次累加的线程ID输出来,会发现,原子累加的线程分布要分散很多。


回到问题上来,为什么我们会一直认为原子操作比加锁要快呢?文中的例子是很特别很特别的,正常业务场景下,我们累加过后,要经过很多业务代码逻辑才会再次去累加,这里已经跨过很多个CPU时间片了。从而同步累加器很难一直获取到锁,这中情况下,同步累加器即会有等待加锁的性能损失还会有缓存一致性带来的性能损失。所以在一般的情况下,同步累加器会慢很多。





再谈Java原子变量以及同步的效率 -- 颠覆你的人生观