我們認為,由于思維定式原子變量總是比同步運行的速度更快,我想是這樣也已經,直到實現了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() {@Overridepublic 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() {@Overridepublic 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):8580Performance 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):5749Performance 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時間片了。從而同步累加器非常難一直獲取到鎖。這中情況下,同步累加器即會有等待加鎖的性能損失還會有緩存一致性帶來的性能損失。
所以在一般的情況下,同步累加器會慢非常多。
版權聲明:本文博客原創文章。博客,未經同意,不得轉載。