阿里架構師告訴你一些多線程的使用技巧

Java中線程的狀態

NEW、RUNNABLE(RUNNING or READY)、BLOCKED、WAITING、TIME_WAITING、TERMINATED

圖片描述(最多50字)

Java將操做系統中的運行和就緒兩個狀態合併稱爲運行狀態。阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態,可是阻塞在JUC包中Lock接口的線程狀態倒是等待狀態,由於JUC中Lock接口對於阻塞的實現是經過LockSupport類中的相關方法實現的。

線程的優先級

Java中線程的優先級分爲1-10這10個等級,若是小於1或大於10則JDK拋出IllegalArgumentException()的異常,默認優先級是5。在Java中線程的優先級具備繼承性,好比A線程啓動B線程,則B線程的優先級與A是同樣的。注意程序正確性不能依賴線程的優先級高低,由於操做系統能夠徹底不理會Java線程對於優先級的決定。

守護線程

Java中有兩種線程:一種是用戶線程,另外一種是守護線程。當進程中不存在非守護線程了,則守護線程自動銷燬。經過setDaemon(true)設置線程爲後臺線程。注意thread.setDaemon(true)必須在thread.start()以前設置,不然會報IllegalThreadStateException異常;在Daemon線程中產生的新線程也是Daemon的;在使用ExecutorService等多線程框架時,會把守護線程轉換爲用戶線程,而且也會把優先級設置爲Thread.NORM_PRIORITY。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。

構造線程

一個新構造的線程對象是由其parent線程來進行空間分配的,而child線程繼承了parent是否爲Daemon、優先級、ThreadGroup、加載資源的contextClassLoader以及可繼承的ThreadLocal(InheritableThreadLocal)、同時還會分配一個惟一的ID來標識這個child線程。

同步不具有繼承性

當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。同步不具備繼承性(聲明爲synchronized的父類方法A,在子類中重寫以後並不具有synchronized的特性)。

使用多線程的方式

extends Thread
implements Runnable
使用Future和Callablejava

Executor框架使用Runnable做爲基本的任務表示形式。Runnable是一種有很大侷限的抽象,雖然run能寫入到日誌文件或者將結果放入某個共享的數據結構,但它不能返回一個值或拋出一個受檢查的異常。許多任務實際上都是存在延遲的計算——執行數據庫查詢,從網絡上獲取資源,或者計算某個複雜的功能。對於這些任務,Callable是一種更好的抽象:它認爲主入口點(call())將返回一個值,並可能拋出一個異常。Runnable和Callable描述的都是抽象的計算任務。這些任務一般是有範圍的,即都有一個明確的起始點,而且最終會結束。

Thread.yield()方法

yield()方法的做用是放棄當前的CPU資源,將它讓給其餘的任務去佔用CPU執行時間。但放棄時間不肯定,有可能剛剛放棄,立刻又得到CPU時間片。這裏須要注意的是yield()方法和sleep()方法同樣,線程並不會讓出鎖,和wait()不一樣,這一點也是爲何sleep()方法被設計在Thread類中而不在Object類中的緣由。

Thread.sleep(0)

在線程中,調用sleep(0)能夠釋放CPU時間,讓線程立刻從新回到就緒隊列而非等待隊列,sleep(0)釋放當前線程所剩餘的時間片(若是有剩餘的話),這樣可讓操做系統切換其餘線程來執行,提高效率。

The semantics of Thread.yield and Thread.sleep(0) are undefined [JLS17.9]; the JVM is free to implement them as no-ops or treat them as scheduling hints. In particular, they are not required to have the semantics of sleep(0) on Unix systems — put the current thread at the end of the run queue for that priority, yielding to other threads of the same priority — though some JVMs implement yield in this way.面試

Thread.join()

若是一個線程A執行了thread.join語句,其含義是:當前線程A等待thread線程終止以後才從thread.join()返回。join與synchronized的區別是:join在內部使用wait()方法進行等待,而synchronized關鍵字使用的是「對象監視器」作爲同步。join提供了另外兩種實現方法:join(long millis)和join(long millis, int nanos),至多等待多長時間而退出等待(釋放鎖),退出等待以後還能夠繼續運行。內部是經過wait方法來實現的。

wait, notify, notifyAll用法

只能在同步方法或者同步塊中使用wait()方法。在執行wait()方法後,當前線程釋放鎖(這點與sleep和yield方法不一樣)。調用了wait函數的線程會一直等待,直到有其它線程調用了同一個對象的notify或者notifyAll方法才能被喚醒,須要注意的是:被喚醒並不表明馬上得到對象的鎖,要等待執行notify()方法的線程執行完,即退出synchronized代碼塊後,當前線程纔會釋放鎖,而呈wait狀態的線程才能夠獲取該對象鎖。若是調用wait()方法時沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,所以,不須要try-catch語句進行捕獲異常。notify()方法只會(隨機)喚醒一個正在等待的線程,而notifyAll()方法會喚醒全部正在等待的線程。若是一個對象以前沒有調用wait方法,那麼調用notify方法是沒有任何影響的。帶參數的wait(long timeout)或者wait(long timeout, int nanos)方法的功能是等待某一時間內是否有線程對鎖進行喚醒,若是超過這個時間則自動喚醒。

setUncaughtExceptionHandler

當單線程的程序發生一個未捕獲的異常時咱們能夠採用try….catch進行異常的捕獲,可是在多線程環境中,線程拋出的異常是不能用try….catch捕獲的,這樣就有可能致使一些問題的出現,好比異常的時候沒法回收一些系統資源,或者沒有關閉當前的鏈接等等。Thread的run方法是不拋出任何檢查型異常的,可是它自身卻可能由於一個異常而被停止,致使這個線程的終結。在Thread ApI中提供了UncaughtExceptionHandler,它能檢測出某個因爲未捕獲的異常而終結的狀況。

thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){});數據庫

一樣能夠爲全部的Thread設置一個默認的UncaughtExceptionHandler,經過調用Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法,這是Thread的一個static方法。在線程池中,只有經過execute()提交的任務,才能將它拋出的異常交給UncaughtExceptionHandler,而經過submit()提交的任務,不管是拋出的未檢測異常仍是已檢查異常,都將被認爲是任務返回狀態的一部分。若是既包含setUncaughtExceptionHandler又包含setDefaultUncaughtExceptionHandler,那麼會被setUncaughtExceptionHandler處理,setDefaultUncaughtExceptionHandler則忽略。

關閉鉤子

JVM既能夠正常關閉也能夠強制關閉,或者說非正常關閉。關閉鉤子能夠在JVM關閉時執行一些特定的操做,譬如能夠用於實現服務或應用程序的清理工做。關閉鉤子能夠在如下幾種場景中應用:1. 程序正常退出(這裏指一個JVM實例);2.使用System.exit();3.終端使用Ctrl+C觸發的中斷;4. 系統關閉;5. OutOfMemory宕機;6.使用Kill pid命令幹掉進程(注:在使用kill -9 pid時,是不會被調用的)。使用方法(Runtime.getRuntime().addShutdownHook(Thread hook))。

終結器finalize

終結器finalize:在回收器釋放它們後,調用它們的finalize方法,從而保證一些持久化的資源被釋放。在大多數狀況下,經過使用finally代碼塊和顯示的close方法,可以比使用終結器更好地管理資源。惟一例外狀況在於:當須要管理對象,而且該對象持有的資源是經過本地方法得到的。可是基於一些緣由(譬如對象復活),咱們要儘可能避免編寫或者使用包含終結器的類。

管道

在Java中提供了各類各樣的輸入/輸出流Stream,使咱們可以很方便地對數據進行操做,其中管道流(pipeStream)是一種特殊的流,用於在不一樣線程間直接傳送數據。一個線程發送數據到輸出管道,另外一個線程從輸入管道中讀數據,經過使用管道,實現不一樣線程間的通訊,而無須藉助相似臨時文件之類的東西。在JDK中使用4個類來使線程間能夠進行通訊:PipedInputStream, PipedOutputStream, PipedReader, PipedWriter。使用代碼相似inputStream.connect(outputStream)或outputStream.connect(inputStream)使兩個Stream之間產生通訊鏈接。

幾種進程間的通訊方式

管道( pipe ):管道是一種半雙工的通訊方式,數據只能單向流動,並且只能在具備親緣關係的進程間使用。進程的親緣關係一般是指父子進程關係。
有名管道 (named pipe) : 有名管道也是半雙工的通訊方式,可是它容許無親緣關係進程間的通訊。
信號量( semophore ) : 信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。
消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
信號 ( sinal ) : 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。
共享內存( shared memory ) :共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
套接字( socket ) : 套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣及其間的進程通訊。網絡

synchronized的類鎖與對象鎖

類鎖:在方法上加上static synchronized的鎖,或者synchronized(xxx.class)的鎖。以下代碼中的method1和method2:

對象鎖:參考method4,method5,method6。

public class LockStrategy
{數據結構

public Object object1 = new Object();
public static synchronized void method1(){}
public void method2(){
    synchronized(LockStrategy.class){}
}
public synchronized void method4(){}
public void method5()
{
    synchronized(this){}
}
public void method6()
{
    synchronized(object1){}
}

}多線程

注意方法method4和method5中的同步塊也是互斥的。

下面作一道習題來加深一下對對象鎖和類鎖的理解,有一個類這樣定義:

public class SynchronizedTest
{架構

public synchronized void method1(){}
public synchronized void method2(){}
public static synchronized void method3(){}
public static synchronized void method4(){}

}併發

那麼,有SynchronizedTest的兩個實例a和b,對於一下的幾個選項有哪些能被一個以上的線程同時訪問呢?

A. a.method1() vs. a.method2()

B. a.method1() vs. b.method1()

C. a.method3() vs. b.method4()

D. a.method3() vs. b.method3()

E. a.method1() vs. a.method3()

答案是什麼呢?BE。

ReentrantLock

ReentrantLock提供了tryLock方法,tryLock調用的時候,若是鎖被其餘線程持有,那麼tryLock會當即返回,返回結果爲false;若是鎖沒有被其餘線程持有,那麼當前調用線程會持有鎖,而且tryLock返回的結果爲true。

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit)app

能夠在構造ReentranLock時使用公平鎖,公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的前後順序來一次得到鎖。synchronized中的鎖時非公平的,默認狀況下ReentrantLock也是非公平的,可是能夠在構造函數中指定使用公平鎖。

ReentrantLock()
ReentrantLock(boolean fair)框架

對於ReentrantLock來講,還有一個十分實用的特性,它能夠同時綁定多個Condition條件,以實現更精細化的同步控制。ReentrantLock使用方式以下:

Lock lock = new ReentrantLock();

lock.lock();
try{
}finally{
    lock.unlock();
}
在finally塊中釋放鎖,目的是保證在獲取到鎖以後,最終可以釋放。不要將獲取鎖的過程寫在try塊中,由於若是在獲取鎖時發生了異常,異常拋出的同時也會致使鎖無端釋放。IllegalMonitorStateException。

公平鎖和非公平鎖只有兩處不一樣

非公平鎖在調用 lock 後,首先就會調用 CAS 進行一次搶鎖,若是這個時候恰巧鎖沒有被佔用,那麼直接就獲取到鎖返回了。

非公平鎖在 CAS 失敗後,和公平鎖同樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,若是發現鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,可是公平鎖會判斷等待隊列是否有線程處於等待狀態,若是有則不去搶鎖,乖乖排到後面。

公平鎖和非公平鎖就這兩點區別,若是這兩次 CAS 都不成功,那麼後面非公平鎖和公平鎖是同樣的,都要進入到阻塞隊列等待喚醒。相對來講,非公平鎖會有更好的性能,由於它的吞吐量比較大。固然,非公平鎖讓獲取鎖的時間變得更加不肯定,可能會致使在阻塞隊列中的線程長期處於飢餓狀態。

synchronized

在Java中,每一個對象都有兩個池,鎖(monitor)池和等待池:

鎖池(同步隊列SynchronizedQueue):假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權,可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。
等待池(等待隊列WaitQueue):假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(由於wait()方法必須出如今synchronized中,這樣天然在執行wait()方法以前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。若是另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會所有進入該對象的鎖池中,準備爭奪鎖的擁有權。若是另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池。

synchronized修飾的同步塊使用monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。不管採用哪一種方式,其本質上是對一個對象的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到synchronized所保護對象的監視器。任意一個對象都擁有本身的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程將會阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。

任意線程對Object(Synchronized)的訪問,首先要得到Object的監視器。若是獲取失敗,線程進入同步隊列(同步隊列SynchronizedQueue),線程狀態變爲BLOCKED。當訪問Object的前驅(得到了鎖的線程)釋放了鎖,則該釋放操做喚醒阻塞在同步隊列中的線程,使其從新嘗試對監視器的獲取。

圖片描述

圖片描述(最多50字)

wait方法調用後,線程狀態由Runnable變爲WAITING/TIME_WAITING,並將當前線程放置到對象的等待隊列(等待隊列WaitQueue)中。notify()方法是將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll方法是將等待隊列中全部的線程所有移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。

圖片描述

圖片描述(最多50字)

在鎖對象的對象頭中有一個threadId字段,當第一個線程訪問鎖時,若是該鎖沒有被其餘線程訪問過,即threadId字段爲空,那麼JVM讓其持有偏向鎖,並將threadId字段的值設置爲該線程的ID。當下一次獲取鎖時,會判斷當前線程ID是否與鎖對象的threadId一致。若是一致,那麼該線程不會再重複獲取鎖,從而提升了程序的運行效率。若是出現鎖的競爭狀況,那麼偏向鎖會被撤銷並升級爲輕量級鎖。若是資源的競爭很是激烈,會升級爲重量級鎖。

Condition

一個Condition和一個Lock關聯在一塊兒,就像一個條件隊列和一個內置鎖相關聯同樣。要建立一個Condition,能夠在相關聯的Lock上調用Lock.newCondition方法。正如Lock比內置加鎖提供了更爲豐富的功能,Condition一樣比內置條件隊列提供了更豐富的功能:在每一個鎖上可存在多個等待、條件等待能夠是可中斷的或者不可中斷的、基於時限的等待,以及公平的或非公平的隊列操做。對於每一個Lock,能夠有任意數量的Condition對象。Condition對象繼承了相關的Lock對象的公平性,對於公平的鎖,線程會依照FIFO順序從Condition.await中釋放。注意:在Condition對象中,與wait,notify和notifyAll方法對應的分別是await,signal,signalAll。可是Condition對Object進行了擴展,於是它也包含wait和notify方法。必定要確保使用的版本——await和signal。

Condition接口的定義:

public interface Condition{

void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUniterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();

}

AQS 中有一個同步隊列(CLH),用於保存等待獲取鎖的線程的隊列。這裏咱們引入另外一個概念,叫等待隊列(condition queue)。

圖片描述

圖片描述(最多50字)

基本上,把這張圖看懂,你也就知道 condition 的處理流程了:1. 咱們知道一個 ReentrantLock 實例能夠經過屢次調用 newCondition() 來產生多個 Condition 實例,這裏對應 condition1 和 condition2。注意,ConditionObject 只有兩個屬性 firstWaiter 和 lastWaiter;2. 每一個 condition 有一個關聯的等待隊列,如線程 1 調用 condition1.await() 方法便可將當前線程 1 包裝成 Node 後加入到等待隊列中,而後阻塞在這裏,不繼續往下執行,等待隊列是一個單向鏈表;3. 調用 condition1.signal() 會將condition1 對應的等待隊列的 firstWaiter 移到同步隊列的隊尾,等待獲取鎖,獲取鎖後 await 方法返回,繼續往下執行。

ReentrantLock與synchonized區別

ReentrantLock能夠中斷地獲取鎖(void lockInterruptibly() throws InterruptedException)
ReentrantLock能夠嘗試非阻塞地獲取鎖(boolean tryLock())
ReentrantLock能夠超時獲取鎖。經過tryLock(timeout, unit),能夠嘗試得到鎖,而且指定等待的時間。
ReentrantLock能夠實現公平鎖。經過new ReentrantLock(true)實現。
ReentrantLock對象能夠同時綁定多個Condition對象,而在synchronized中,鎖對象的的wait(), notify(), notifyAll()方法能夠實現一個隱含條件,若是要和多於一個的條件關聯的對象,就不得不額外地添加一個鎖,而ReentrantLock則無需這樣作,只須要屢次調用newCondition()方法便可。

Lock接口中的方法:

void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();

重入鎖的實現原理

爲每一個鎖關聯一個請求計數和佔有他的線程

synchronized與ReentrantLock之間進行選擇

ReentrantLock與synchronized相比提供了許多功能:定時的鎖等待,可中斷的鎖等待、公平鎖、非阻塞的獲取鎖等,並且從性能上來講ReentrantLock比synchronized略有勝出(JDK6起),在JDK5中是遠遠勝出,爲嘛不放棄synchronized呢?ReentrantLock的危險性要比同步機制高,若是忘記在finally塊中調用unlock,那麼雖然代碼表面上能正常運行,但實際上已經埋下了一顆定時炸彈,並極可能傷及其餘代碼。僅當內置鎖不能知足需求時,才能夠考慮使用ReentrantLock。

讀寫鎖ReentrantReadWriteLock

讀寫鎖表示也有兩個鎖,一個是讀操做相關的鎖,也稱爲共享鎖;另外一個是寫操做相關的鎖,也叫排它鎖。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。在沒有Thread進行寫操做時,進行讀取操做的多個Thread均可以獲取讀鎖,而進行寫入操做的Thread只有在獲取寫鎖後才能進行寫入操做。即多個Thread能夠同時進行讀取操做,可是同一時刻只容許一個Thread進行寫入操做。(lock.readlock.lock(), lock.readlock.unlock, lock.writelock.lock, lock.writelock.unlock)

鎖降級是指寫鎖降級成讀鎖。若是當前線程擁有寫鎖,而後將其釋放,最後獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,最後釋放(先前擁有的)寫鎖的過程。鎖降級中的讀鎖是否有必要呢?答案是必要。主要是爲了保證數據的可見性,若是當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另外一個線程(T)獲取了寫鎖並修改了數據,那麼當前線程沒法感知線程T的數據更新。若是當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據並釋放讀鎖以後,線程T才能獲取寫鎖進行數據更新。

Happens-Before規則

程序順序規則:若是程序中操做A在操做B以前,那麼在線程中A操做將在B操做以前。
監視器鎖規則:一個unlock操做現行發生於後面對同一個鎖的lock操做。
volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。
線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做。
線程終止規則:線程的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等於段檢測到線程已經終止執行。
線程中斷規則:線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
終結器規則:對象的構造函數必須在啓動該對象的終結器以前執行完成。
傳遞性:若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

注意:若是兩個操做之間存在happens-before關係,並不意味着java平臺的具體實現必需要按照Happens-Before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法。

重排序

是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。

as-if-serial

無論怎麼重排序,單線程程序的執行結構不能被改變。

說到最後給你們免費分享一波福利吧!我本身收集了一些Java資料,裏面就包涵了一些BAT面試資料,以及一些 Java 高併發、分佈式、微服務、高性能、源碼分析、JVM等技術資料

資料獲取方式:請加羣BAT架構技術交流羣:171662117

相關文章
相關標籤/搜索