atomic 包、synchronized | Java 中執行緒安全
相關閱讀
徹底搞懂 CPU 中的記憶體結構
Java 記憶體模型 ,一篇就夠了!
多執行緒實現原理
之前已經說過了,我們在保證執行緒安全的時候主要就是保證執行緒安全的 3 大特性,原子性、可見性、有序性、而在 JMM 的層面也已經做了相關的操作,比方說 JMM 定義的 8 種原子操作以及相關的規則,happens-before 原則。
今天主要就來看看 Java 中實現執行緒安全的方法之二,使用 atomic 包,synchronized 關鍵字。
首先說說 AtomicInteger 這個類,我們來看一個例子,計數器。實現很簡單,就是每個執行緒都過來加 1,我們期待的結果是 999,但是若不保證執行緒安全,結果往往不對。
import java.util.concurrent.atomic.AtomicInteger; public class AtomicInt { public static void main(String[] args) { for(int i=0;i < 1000;i ++){ Thread thread = new Thread(new Counter()); thread.start(); } System.out.println(Counter.getCount()); } } class Counter implements Runnable{ //private static int count = 0; private static AtomicInteger count = newAtomicInteger(0); public static int getCount(){ //return count; return count.get(); } @Override public void run() { //count++; count.incrementAndGet(); } }
下面就來分析一下 incrementAndGet 方法的具體實現
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1)+ 1; } // var1 :count 物件; // var2 :加數,count 中封裝的整數; // var4 :被加數 1; public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { // 該方法的主要目的取出主記憶體中的加數,【即為當前 count.get() 值】 var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } public native int getIntVolatile(Object var1, long var2); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
compareAndSwapInt 方法既是我們經常看到的 CAS 的簡寫,但更多的是代表了一種思想。具體在這裡該方法主要目的是比較我們從主存取出的整數值和我們從 count 中傳過來的是否一致,count 中的整數也就是從執行緒的工作記憶體傳過來的。若一致,則計算結果並返回。若不一致,為 var5 賦值【即修改主存中的值】,並繼續從主存中取出 var5 ,繼續比較,直到返回。
這中間的 CAS 的思想在其它的類中也很常用,理解其核心思想即可。比較工作記憶體和主存中的資料,使其一致再進行計算。
與上面類似的還有 AtomicBoolean 和 AtomicLong 物件,底層的實現還是 CAS。但是在 JDK1.8 中,出現了與 AtomicLong 類似的 LongAdder 物件。
我們知道,在 CAS 的實現中,主體部分在一個 while 迴圈中,會一直找到工作記憶體和主存一致的情況,若是競爭不激烈,這是沒問題的,但是當競爭非常激烈的時候,一直返回不了結果,效能就會很差。CAS 也是一種樂觀鎖的表現,以為可以很快的找到並返回結果。
對於普通的 long 和 double 變數,JVM 必須將 64 位的讀寫操作拆成 2 個 32 位讀寫操作。而 LongAdder 這個類的實現基於的思想就是將【熱點資料分離】,比方說可以將 AtomicLong 內部的 value 分離成一個數組,不同的執行緒根據 hash 可以操作不同的 cell ,最後再將整個陣列中所有 cell 中的數累加。
這樣做的結果就相當於在 AtomicLong 的基礎上,將單點的壓力,分散到不同的 cell 中。在低併發的時候可以不分離熱點資料,使用 base 資料, 在高併發的分離資料,這樣就保證了效能。缺點是 LongAdder 在統計的時候如果有併發更新,可能導致統計的資料有誤差。
我們知道在 CAS 中,我們會持續的判斷記憶體中的數和工作記憶體中是否一致,以此來判斷有沒有其它的執行緒修改了工作記憶體中的資料,但是存在一種情況,共享變數是 1 被修改為 2 ,而後又被修改為 1 ,此時已經執行緒不安全了。這個問題就是常說的 ABA 問題。
Java 中提供了 AtomicStampedReference<T>,這個類主要解決的就是 CAS 中 ABA 問題,為了解決 ABA 問題,引入了一個‘變數版本號’的概念,即每次修改版本號都會加 1。使用一個變數 stamp 來記錄變數版本號。
AtomicBoolean 這個類中的 compareAndSet 方法還是比較常用的。用在標識變數 flag。若是某段程式碼只需要執行一次可以使用這個方法來做。
public class AtomicBooleanTest { private static AtomicBoolean flag = newAtomicBoolean(false); public static void main(String[] args) { // 不管有多少個執行緒在執行,都能保證只有會一個執行緒執行下面這段程式碼 // 如果 flag 是 false,修改為 true if(flag.compareAndSet(false, true)){ System.out.println("the flag has beenchanged ~"); // Work(); } } }
Java 中的鎖有兩種,一種是 synchronized 關鍵字,依賴於 JVM ,還有一種是程式碼層面的 Lock,依賴於特殊的 CPU 指令,常用的實現類有 ReentrantLock。
synchronized 可以用來修飾程式碼塊和方法,而鎖住的物件又可分為當前物件和類物件。當修飾靜態方法和程式碼塊(類.class)時為類鎖。修飾一般方法和程式碼塊(this)時為物件鎖。
舉例看看物件鎖
public class SynTest { public static void main(String args[]) { SyncThread s = new SyncThread(); Thread t1 = new Thread(s); Thread t2 = new Thread(s); t1.start(); t2.start(); } } class SyncThread implements Runnable { private static int count = 0; public void run() { // 修飾程式碼塊,鎖住當前物件:一個執行緒訪問一個物件中的synchronized(this) 同步程式碼塊時,其他試圖訪問該物件的執行緒將被阻塞 synchronized (this) { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName()+ ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) {} } } } }
另外 JMM 關於 synchronized 有兩條規定:1 執行緒解鎖前,必須把共享變數的最新值重新整理到主存。2 執行緒解鎖前,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主存中重新讀取最新的值(注意,加鎖與解鎖是同一把鎖)這也就保障了可見性。