注:ifeve.com的同名文章為本人所發,此文在其基礎做了些調整。轉載請注明出處!
一、java8中CAS的增強
前些天,我偶然地將之前寫的用來測試AtomicInteger和synchronized的自增性能的代碼跑了一下,意外地發現AtomicInteger的性能比synchronized更好了,經過一番原因查找,有了如下發現:
在jdk1.7中,AtomicInteger的getAndIncrement是這樣的:
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; }}public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
而在jdk1.8中,是這樣的:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}
可以看出,在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,沒有此方法?;究梢詳喽ǎ琔nsafe新增的方法是性能提升的關鍵。(文章末尾將附上一些探索的過程及推論)
通過查看AtomicInteger的源碼可以發現,受影響的還有getAndAdd、addAndGet等大部分方法。
結論:有了這次對CAS的增強,我們又多了一個使用非阻塞算法的理由。
二、測試方法
以下給出測試代碼,供參考與測試。需要注意的是,此測試方法簡單粗暴,compareAndSet的性能不如synchronized,并不能簡單地說synchronized就更好,兩者的使用方式是存在差異的,而且在實際使用中,還有業務處理,不可能有如此高的競爭強度,此對比僅作為一個參考,該測試能夠證明的是,AtomicInteger.getAndIncrement的性能有了大幅提升。
package performance;import java.util.concurrent.CountDownLatch;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.locks.LockSupport;/** * @author trytocatch@163.com */public class AtomicTest { //測試規模,調用一次getAndIncreaseX視作提供一次業務服務,記錄提供TEST_SIZE次服務的耗時 PRivate static final int TEST_SIZE = 100000000; //客戶線程數 private static final int THREAD_COUNT = 10; //使用CountDownLatch讓各線程同時開始 private CountDownLatch cdl = new CountDownLatch(THREAD_COUNT + 1); private int n = 0; private AtomicInteger ai = new AtomicInteger(0); private long startTime; public void init() { startTime = System.nanoTime(); } /** * 使用AtomicInteger.getAndIncrement,測試結果為1.8比1.7有明顯性能提升 * @return */ private final int getAndIncreaseA() { int result = ai.getAndIncrement(); if (result == TEST_SIZE) { System.out.println(System.nanoTime() - startTime); System.exit(0); } return result; } /** * 使用synchronized來完成同步,測試結果為1.7和1.8幾乎無性能差別 * @return */ private final int getAndIncreaseB() { int result; synchronized (this) { result = n++; } if (result == TEST_SIZE) { System.out.println(System.nanoTime() - startTime); System.exit(0); } return result; } /** * 使用AtomicInteger.compareAndSet在java代碼層面做失敗重試(與1.7的AtomicInteger.getAndIncrement的實現類似), * 測試結果為1.7和1.8幾乎無性能差別 * @return */ private final int getAndIncreaseC() { int result; do { result = ai.get(); } while (!ai.compareAndSet(result, result + 1)); if (result == TEST_SIZE) { System.out.println(System.nanoTime() - startTime); System.exit(0); } return result; } public class MyTask implements Runnable { @Override public void run() { cdl.countDown(); try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } while (true) getAndIncreaseA();// getAndIncreaseB(); } } public static void main(String[] args) throws InterruptedException { AtomicTest at = new AtomicTest(); for (int n = 0; n < THREAD_COUNT; n++) new Thread(at.new MyTask()).start(); System.out.println("start"); at.init(); at.cdl.countDown(); }}
以下是在Intel(R) Core(TM) i7-4710HQ CPU @2.50GHz(四核八線程)下的測試結果(波動較小,所以每項只測試了四五次,取其中一個較中間的值):
jdk1.7AtomicInteger.getAndIncrement 12,653,757,034synchronized 4,146,813,462AtomicInteger.compareAndSet 12,952,821,234jdk1.8AtomicInteger.getAndIncrement 2,159,486,620synchronized 4,067,309,911AtomicInteger.compareAndSet 12,893,188,541
三、提升原因的探索及推論
一開始,我懷疑在1.8中,Unsafe直接使用了native方法,而1.7是在getAndIncrement里完成的失敗重試,也就是在java代碼層面,所以造成了性能的差別,于是我用jad反編譯了Unsafe,得到了如下代碼:
public final int getAndAddInt(Object obj, long l, int i){ int j; do j = getIntVolatile(obj, l); while(!compareAndSwapInt(obj, l, j, j + i)); return j;}public native int getIntVolatile(Object obj, long l);public final native boolean compareAndSwapInt(Object obj, long l, int i, int j);
并且參考了openjdk8的Unsafe源碼:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v;}public native int getIntVolatile(Object o, long offset);public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
由上面的信息可以看出,1.8中,失敗重試也是在java代碼層面進行的(區別是轉移到了Unsafe的java方法里面),算是推翻了我的猜測,于是我決定通過反射,直接獲取到Unsafe實例,編寫跟Unsafe.getAndAddInt方法一樣的代碼來測試,看能否找到一些新的線索:
...import sun.misc.Unsafe;public class AtomicTest { .... private Unsafe unsafe; private long valueOffset; public AtomicTest(){ Field f; try { f = Unsafe.class.getDeclaredField("theUnsafe"); f.setaccessible(true); unsafe = (Unsafe)f.get(null); valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); }catch(NoSuchFieldException e){ ... } } private final int getAndIncreaseD(){ int result; do{ result = unsafe.getIntVolatile(ai, valueOffset); }while(!unsafe.compareAndSwapInt(ai, valueOffset, result, result+1)); if(result == MAX){ System.out.println(System.nanoTime()-startTime); System.exit(0); } return result; } ...}
但讓人失望的是,該方式跟1.7的getAndIncrement效率一樣,明明跟1.8的Unsafe.getAndAddInt方法一樣,卻是截然不同的效率。
最后,經過ifeve.com的網友們的指點,對性能的提升原因有了如下推論,雖不敢說百分之百正確(因為沒有用jvm的源碼作為論據),但還是有很大把握的,感謝網友@周 可人和@liuxinglanyue!
Unsafe是經過特殊處理的,不能理解成常規的java代碼,區別在于:
這也正好跟openjdk8中Unsafe::getAndAddInt上方的注釋相吻合:
// The following contain CAS-based Java implementations used on// platforms not supporting native instructions
相關鏈接:http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.htmlhttp://hg.openjdk.java.net/jdk8u/hs-dev/jdk/file/a006fa0a9e8f/src/share/classes/sun/misc/Unsafe.java
新聞熱點
疑難解答