AtomicInteger的使用
在之前一篇volatile學習里面提到過了,volatile修飾的變量只是保證內存可見性,無法保證原子性,可能出現寫沖突。要想保證線程安全,需要使用AtomicInteger。具體代碼如下:
public class AtomicTest { public static AtomicInteger race=new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); } private static final int THREADS_COUNT=20; public static void main(String[] args){ Thread[] threads=new Thread[THREADS_COUNT]; for(int i=0;i<THREADS_COUNT;i++){ threads[i]=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<10000;i++){ increase(); } } }); threads[i].start(); } while(Thread.activeCount()>1){ Thread.yield(); System.out.println(race); } } }
運行結果為200000,若是把變量用volatile修飾,然后increase方法替換成race++,那么最后得到的結果基本不為200000,且每一次得到的結果都可能是不同的。AtomicInteger的incrementAndGet是通過unsafe的getAndAddInt來實現的,而后者的代碼如下:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
其中var1是調用unsafe的AtomicInteger對象,而var2是用來記錄value本身在內存的編譯地址的,這個記錄,也主要是為了在更新操作在內存中找到value的位置,方便比較。而var5則是AtomicInteger的value值,被valotile修飾,當沖突時因為內存可見性,操作失敗的線程可以看到修改成功的線程修改后的最新值。compareAndSwapInt亦即CAS,是原子操作,它的作用是若原值為var5即在這之間沒發生過修改,那么設置為新值var5 + var4。AtomicInteger方法不是互斥同步的,而是非阻塞同步,也就是樂觀鎖,而前者被稱為悲觀鎖。
悲觀鎖認為沖突經常會發生,所以直接上鎖,強制所有線程一次有且只有一條線程能操作,嚴格保證串行性。而樂觀鎖認為沖突很少發生,先進行操作,之后檢查有沒有沖突,若發生了沖突再采取其他補償措施(最常見的補償措施就是不斷地重試,直到成功為止)。兩者並無優劣之分,使用場景不同而已。若是沖突經常發生,那么采用樂觀鎖場景中,每次操作只有一條線程成功,失敗的線程會不斷嘗試然后不斷沖突,性能可能很差,但若是沖突很少發生,那么悲觀鎖那樣一次只允許一條線程讀寫那么效率就太低了。樂觀鎖是通過檢查版本號來檢測是否發生沖突的,而版本號每次修改成功時+1。你每次需要修改值時帶上版本號,若是你的版本號與當前一致那么修改成功,若是中間被其他線程修改了,那么版本號就與你的不一樣了,你只能請求最新的版本號然后嘗試着再次修改直到成功為止。像版本號這樣單向增加的比較好,若是用原值來作為比較的標准的話,有可能會發生中間有人把原值加1另外一人把原值減1,然后輪到你的時候你以為原值在這中間沒有變化而可能產生歧義,亦即ABA問題。而版本號只要發生了修改都會增加而無論操作是什么。