從建立以來,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關聯
排序,這解釋了一個線程什麼時候可以看到另外一個線程操做的結果,以及是什麼構成了一個同步良好的程序。微信
happens-before關聯
有如下屬性:數據結構
Thread#start
的方法在線程的全部操做以前執行volatile
變量的操做在全部後序讀取該變量的操做以前執行。final
型變量的操做在發佈該對象的引用以前執行Thread#join
方法返回以前執行上圖中,Action X
在Action Y
以前執行,所以線程1
在Action X
之前執行的全部操做對線程2
在Action Y
以後的全部操做可見。多線程
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/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
方法,你首先須要獲取對象的鎖notifyAll
而致使的順序問題。並且它還防止線程因爲僞喚起繼續執行。notify/notifyAll
以前已經知足了等待條件。若是不這樣的話,將只會發出一個喚醒通知,可是在該等待條件上的線程永遠沒法跳出其等待循環。博主備註:這裏解釋一下爲什麼建議將wait
放在條件性循環中、假設如今有一個線程,並無將wait放入條件性循環中,代碼以下:
class UnconditionLoop{ private boolean condition; synchronized void waitForCondition() throws InterruptedException{ //.... wait(); } synchronized void satisfyCondition(){ condition = true; notifyAll(); } }
假設如今有兩個線程分別同時調用waitForCondition
和satisfyCondition()
,而調用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
喚醒全部等待的線程,那麼waitForCondition
和waitForCondition2
都將會被喚醒繼續執行。而waitForCondition
的條件並無被知足!
所以在條件中循環等待信號是有必要的。
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(); } }
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."); } } }
AtomicInteger
和AtomicLong
都用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/AtomicInteger
,LongAdder
在多個單元格中維護該值,並在須要時對這些值同時遞增,從而在高併發的狀況下性能更好。
在線程中包含數據而且不須要鎖定的一種方法是使用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; } }
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~