貓頭鷹的深夜翻譯:核心JAVA併發(一)

簡介

從建立以來,JAVA就支持核心的併發概念如線程和鎖。這篇文章會幫助從事多線程編程的JAVA開發人員理解核心的併發概念以及如何使用它們。java

(博主將在其中加上本身的理解以及本身想出的例子做爲補充)面試

概念

原子性:原子操做是指該系列操做要麼所有執行,要麼所有不執行,所以不存在部分執行的狀態。
可見性:一個線程可以看見另外一個線程所帶來的改變。

競爭狀況

當多個線程在一個共享的資源上執行一組操做時,會產生競爭。根據各個線程執行操做的順序可能產生多個不一樣結果。下面的代碼不是線程安全的,value可能會被初始化屢次,由於check-then-act型(先判斷是否爲null,而後初始化)的惰性初始化並不是原子性操做編程

class Lazy <T> {
  private volatile T value;
  T get() {
    if (value == null)
      value = initialize();
    return value;
  }
}

數據衝突

當兩個或多個線程在沒有同步的狀況下試圖訪問同一個非final變量時,會產生數據衝突。不使用同步可能使數據的改變對別的線程不可見,從而可能讀取過時的數據,並致使如無限循環,數據結構損壞和不許確的計算等後果。下面這段代碼可能會致使無限循環,由於讀者線程可能永遠都沒有看到寫入者線程作出的更改:安全

class Waiter implements Runnable {
  private boolean shouldFinish;
  void finish() { shouldFinish = true; }
  public void run() {
    long iteration = 0;
    while (!shouldFinish) {
      iteration++;
    }
    System.out.println("Finished after: " + iteration);
  }
}
class DataRace {
  public static void main(String[] args) throws InterruptedException {
    Waiter waiter = new Waiter();
    Thread waiterThread = new Thread(waiter);
    waiterThread.start();
    waiter.finish();
    waiterThread.join();
  }
}

JAVA內存模型:happens-before關係

JAVA內存模型是根據讀寫字段等操做來定義的,並在控制器上進行同步。操做根據happens-before關聯排序,這解釋了一個線程什麼時候可以看到另外一個線程操做的結果,以及是什麼構成了一個同步良好的程序。微信

happens-before關聯有如下屬性:數據結構

  • Thread#start的方法在線程的全部操做以前執行
  • 在釋放當前控制器以後,後序的請求才能夠獲取控制器。(Releasing a monitor happens before any subsequent acquisition of the same monitor.)
  • 寫入volatile變量的操做在全部後序讀取該變量的操做以前執行。
  • 寫入final型變量的操做在發佈該對象的引用以前執行
  • 線程的全部操做在從Thread#join方法返回以前執行

clipboard.png

上圖中,Action XAction Y以前執行,所以線程1Action X之前執行的全部操做對線程2Action Y以後的全部操做可見。多線程

標註的同步功能

synchronized關鍵字

synchronized關鍵字用來防止不一樣的線程同時進入一段代碼。它確保了你的操做的原子性,由於你只有得到了這段代碼的鎖才能進入這段代碼,使得該鎖所保護的數據能夠在獨佔模式下操做。除此之外,它還確保了別的線程在得到了一樣的鎖以後,可以觀察到以前線程的操做。併發

class AtomicOperation {
  private int counter0;
  private int counter1;
  void increment() {
    synchronized (this) {
      counter0++;
      counter1++;
    }
  }
}

synchronized關鍵字也能夠在方法層上聲明。app

靜態方法:將持有該方法的類做爲加鎖對象
非靜態方法:加鎖 this指針

鎖是可重入的。因此若是一個線程已經持有了該鎖,它能夠一直訪問該鎖下的任何內容:高併發

class Reentrantcy {
  synchronized void doAll() {
    doFirst();
    doSecond();
  }
  synchronized void doFirst() {
    System.out.println("First operation is successful.");
  }
  synchronized void doSecond() {
    System.out.println("Second operation is successful.");
  }
}

爭用程度影響如何得到控制器:

初始化:剛剛建立,沒有被獲取
biased:鎖下的代碼只被一個線程執行,不會產生衝突
thin:控制器被幾個線程無衝突的獲取。使用 CAS(compare and swap)來管理這個鎖
fat:產生衝突。JVM請求操做系統互斥,並讓操做系統調度程序處理線程停放和喚醒。

wait/notify

wait/notify/notifyAll方法在Object類中聲明。wait方法用來將線程狀態改變爲WAITING或是TIMED_WAITING(若是傳入了超時時間值)。要想喚醒一個線程,下列的操做均可以實現:

  • 另外一個線程調用notify方法,喚醒在控制器上等待的任意的一個線程
  • 另外一個線程調用notifyAll方法,喚醒在該控制器上等待的全部線程
  • Thread#interrupt方法被調用,在這種狀況下,會拋出InterruptedException

最經常使用的一個模式是一個條件性循環:

class ConditionLoop {
  private boolean condition;
  synchronized void waitForCondition() throws InterruptedException {
    while (!condition) {
      wait();
    }
  }
  synchronized void satisfyCondition() {
    condition = true;
    notifyAll();
  }
}
  • 記住,要想使用對象上的wait/notify/notifyAll方法,你首先須要獲取對象的鎖
  • 老是在一個條件性循環中等待,從而解決若是另外一個線程在wait開始以前知足條件而且調用了notifyAll而致使的順序問題。並且它還防止線程因爲僞喚起繼續執行。
  • 時刻確保你在調用notify/notifyAll以前已經知足了等待條件。若是不這樣的話,將只會發出一個喚醒通知,可是在該等待條件上的線程永遠沒法跳出其等待循環。

博主備註:這裏解釋一下爲什麼建議將wait放在條件性循環中、假設如今有一個線程,並無將wait放入條件性循環中,代碼以下:

class UnconditionLoop{
    private boolean condition;
    
    synchronized void waitForCondition() throws InterruptedException{
        //....
        wait();
    }
    
    synchronized void satisfyCondition(){
        condition = true;
        notifyAll();
    }
}

假設如今有兩個線程分別同時調用waitForConditionsatisfyCondition(),而調用satisfyCondition的方法先調用完成,而且發出了notifyAll通知。鑑於waitForCondition方法根本沒有進入wait方法,所以它就錯過了這個解掛信號,從而永遠沒法被喚醒。

這時你可能會想,那就使用if判斷一下條件唄,若是條件還沒知足,就進入掛起狀態,一旦接收到信號,就能夠直接執行後序程序。代碼以下:

class UnconditionLoop{
    private boolean condition;
    
    private boolean condition2;
    
    synchronized void waitForCondition() throws InterruptedException{
        //....
        if(!condition){
            wait();
        }
    }
    synchronized void waitForCondition2() throws InterruptedException{
        //....
        if(!condition2){
            wait();
        }
    }
    synchronized void satisfyCondition(){
        condition = true;
        notifyAll();
    }
    
    synchronized void satisfyCondition2(){
        condition2 = true;
        notifyAll();
    }
}

那讓咱們再假設這個 方法中還存在另外一個condition,而且也有其對應的等待和喚醒方法。假設這時satisfyConsition2被知足併發出nofityAll喚醒全部等待的線程,那麼waitForConditionwaitForCondition2都將會被喚醒繼續執行。而waitForCondition的條件並無被知足!

所以在條件中循環等待信號是有必要的。


volatile關鍵字

volatile關鍵字解決了可見性問題,而且使值的更改原子化,由於這裏存在一個happens-before關聯:對volatile值的更改會在全部後續讀取該值的操做以前執行。所以,它確保了後序全部的讀取操做可以看到以前的更改。

class VolatileFlag implements Runnable {
  private volatile boolean shouldStop;
  public void run() {
    while (!shouldStop) {
      //do smth
    }
    System.out.println("Stopped.");
  }
  void stop() {
    shouldStop = true;
  }
  public static void main(String[] args) throws InterruptedException {
    VolatileFlag flag = new VolatileFlag();
    Thread thread = new Thread(flag);
    thread.start();
    flag.stop();
    thread.join();
  }
}

Atomics

java.util.concurrent.atomic包中包含了一組支持在單一值上進行多種原子性操做的類,從而從加鎖中解脫出來。

使用AtomicXXX類,能夠實現原子性的check-then-act操做:

class CheckThenAct {
  private final AtomicReference<String> value = new AtomicReference<>();
  void initialize() {
    if (value.compareAndSet(null, "Initialized value")) {
      System.out.println("Initialized only once.");
    }
  }
}

AtomicIntegerAtomicLong都用increment/decrement操做:

class Increment {
  private final AtomicInteger state = new AtomicInteger();
  void advance() {
    int oldState = state.getAndIncrement();
    System.out.println("Advanced: '" + oldState + "' -> '" + (oldState + 1) + "'.");
  }
}
若是你想要建立一個計數器,可是並不須要原子性的讀操做,可使用 LongAdder替代 AtomicLong/AtomicIntegerLongAdder在多個單元格中維護該值,並在須要時對這些值同時遞增,從而在高併發的狀況下性能更好。

ThreadLocal

在線程中包含數據而且不須要鎖定的一種方法是使用ThreadLocal存儲。從概念上將,ThreadLocal就好像是在每一個線程中都有本身版本的變量。ThreadLocal經常使用來存儲只屬於線程本身的值,好比當前的事務以及其它資源。並且,它還能用來維護單個線程專有的計數器,統計或是ID生成器。

class TransactionManager {
  private final ThreadLocal<Transaction> currentTransaction 
      = ThreadLocal.withInitial(NullTransaction::new);
  Transaction currentTransaction() {
    Transaction current = currentTransaction.get();
    if (current.isNull()) {
      current = new TransactionImpl();
      currentTransaction.set(current);
    }
    return current;
  }
}

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索