谈论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;
    }
}


这是一个非常easy的累加器,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时间片了。从而同步累加器非常难一直获取到锁。这中情况下,同步累加器即会有等待加锁的性能损失还会有缓存一致性带来的性能损失。

所以在一般的情况下,同步累加器会慢非常多。





版权声明:本文博客原创文章。博客,未经同意,不得转载。

posted @ 2015-08-18 18:40  hrhguanli  阅读(361)  评论(0编辑  收藏  举报