1. 程式人生 > >Java8中CAS的增強

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中,沒有此方法。(PS:為了找出原因,我反編譯了Unsafe,發現CAS的失敗重試就是在getAndAddInt方法裡完成的,我用反射獲取到Unsafe例項,編寫了跟getAndAddInt相同的程式碼,但測試結果卻跟jdk1.7的getAndIncrement一樣慢,不知道Unsafe裡面究竟玩了什麼黑魔法,還請高人不吝指點)(補充:文章末尾已有推論)

通過檢視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;

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.7
AtomicInteger.getAndIncrement 12,653,757,034
synchronized 4,146,813,462
AtomicInteger.compareAndSet 12,952,821,234

jdk1.8
AtomicInteger.getAndIncrement 2,159,486,620
synchronized 4,067,309,911
AtomicInteger.compareAndSet 12,893,188,541

補充:應網友要求,在此提供Unsafe.getAndAddInt的相關原始碼以及我的測試程式碼。
用jad反編譯jdk1.8中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);

我的測試程式碼(提示:如果eclipse等ide報錯,那是因為使用了受限的Unsafe,可以將警告級別從error降為warning,具體百度即可):

...
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;
	}
	...
}

補充2:對於效能提升的原因,有以下推論,雖不敢說百分之百正確(因為沒有用jvm的原始碼作為論據),但還是有很大把握的,感謝網友@周 可人和@liuxinglanyue!

Unsafe是經過特殊處理的,不能理解成常規的java程式碼,區別在於:
在呼叫getAndAddInt的時候,如果系統底層支援fetch-and-add,那麼它執行的就是native方法,使用的是fetch-and-add;
如果不支援,就按照上面的所看到的getAndAddInt方法體那樣,以java程式碼的方式去執行,使用的是compare-and-swap;
這也正好跟openjdk8中Unsafe::getAndAddInt上方的註釋相吻合:

// The following contain CAS-based Java implementations used on
// platforms not supporting native instructions

Unsafe的特殊處理也就是我上文所說的“黑魔法”。

相關連結:
http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html
http://hg.openjdk.java.net/jdk8u/hs-dev/jdk/file/a006fa0a9e8f/src/share/classes/sun/misc/Unsafe.java