JVM學(xué)習(xí)記錄-線程安全與鎖優(yōu)化(一)
前言
線程:程序流執(zhí)行的最小單元。線程是比進(jìn)程更輕量級(jí)的調(diào)度執(zhí)行單位,線程的引入,可以把一個(gè)進(jìn)程的資源分配和執(zhí)行調(diào)度分開,各個(gè)線程既可以共享進(jìn)程資源(內(nèi)存地址、文件I/O等),又可以獨(dú)立調(diào)度(線程是CPU調(diào)度的基本單位)。
Java語(yǔ)言定義了5中線程狀態(tài),在任意一個(gè)時(shí)間點(diǎn),一個(gè)線程只能有且只有其中的一種狀態(tài),5中狀態(tài)如下。
新建(New):創(chuàng)建后尚未啟動(dòng)的線程處于這種狀態(tài)。
運(yùn)行(Runnable):Runnable包括了操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是處于此狀態(tài)的線程可能正在執(zhí)行,也可能正在等待著CPU為它分配執(zhí)行時(shí)間。
無(wú)限期等待(Waiting):處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被其他線程顯示地喚醒。
讓線程進(jìn)入無(wú)限等待的方法有如下幾個(gè):
- 沒有設(shè)置Timeout參數(shù)的Object.wait()方法。
- 沒有設(shè)置Timeout參數(shù)的Thread.join()方法。
- LockSupport.park()方法。
限期等待(Timed Waiting):處于這種狀態(tài)的線程也不會(huì)被分配CPU執(zhí)行時(shí)間,不過(guò)無(wú)須等待被其他線程顯式地喚醒,在一定時(shí)間之后它們會(huì)由系統(tǒng)自動(dòng)喚醒。
讓線程進(jìn)入限期等待狀態(tài)的方法有如下幾個(gè):
- Thread.sleep()方法。
- 設(shè)置了Timeout參數(shù)的Object.wait()方法。
- 設(shè)置了Timeout參數(shù)的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
阻塞(Blocked):線程被阻塞了,“阻塞狀態(tài)”是在等待著獲取到一個(gè)排他鎖,這個(gè)事件將在另一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;更通俗的解釋就是一個(gè)線程正在干著一件事,沒資源干其他的事,當(dāng)來(lái)了其他的事時(shí)就只能阻塞的等著線程能騰出時(shí)間來(lái)處理。
結(jié)束(Terminated):已終止線程的線程狀態(tài),線程已經(jīng)結(jié)束執(zhí)行。
這5種狀態(tài)在遇到特定的事件的時(shí)候會(huì)相互轉(zhuǎn)換。

線程安全
一個(gè)比較嚴(yán)謹(jǐn)線程安全定義:當(dāng)多個(gè)線程訪問一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確地結(jié)果,那么這個(gè)對(duì)象就是線程安全的。
Java語(yǔ)言中的線程安全
研究線程安全,需要限定于多個(gè)線程之間存在共享數(shù)據(jù)訪問這個(gè)前提。Java語(yǔ)言中各種操作共享數(shù)據(jù)分為以下5類:
不可變
在JDK1.5以后,Java語(yǔ)言中不可變的對(duì)象一定是線程安全的,無(wú)論是對(duì)象的方法實(shí)現(xiàn)還是方法的調(diào)用者,都不需要再采取任何的線程安全保障措施。如果一個(gè)基本數(shù)據(jù)類在定義時(shí)使用final關(guān)鍵字修飾它,就可以保證它時(shí)不可變的。如果final修飾的是一個(gè)對(duì)象,需要保證對(duì)象的方法不會(huì)對(duì)其狀態(tài)產(chǎn)生影響才行。例如:String類的substring()、concat()這些方法不會(huì)影響原來(lái)的值,只會(huì)生成一個(gè)新的字符串。
保證對(duì)象方法不會(huì)對(duì)其狀態(tài)產(chǎn)生影響的實(shí)現(xiàn)方式有很多,最簡(jiǎn)單是將對(duì)象中帶有狀態(tài)的屬性用final修飾。
例如Integer類中的實(shí)現(xiàn)代碼:
/** * The value of the {@code Integer}. * * @serial */ private final int value; /** * Constructs a newly allocated {@code Integer} object that * represents the specified {@code int} value. * * @param value the value to be represented by the * {@code Integer} object. */ public Integer(int value) { this.value = value; }
Java中除了String類、Integer類,還有其他的Long、Double等包裝類,以及BigInteger和BigDecimal等大數(shù)據(jù)類型,都符合不可變要求的類型。
絕對(duì)線程安全
絕對(duì)線程安全,是指絕對(duì)的符合前面提到的線程安全的定義,多線程永遠(yuǎn)調(diào)用對(duì)象時(shí)永遠(yuǎn)都能獲得正確的結(jié)果。但是為了實(shí)現(xiàn)這個(gè)絕對(duì)要付出的代價(jià)是很大的,在Java中標(biāo)注自己是線程安全的類,絕大多數(shù)都不是絕對(duì)線程安全的,例如Vector類,java.util.Vector是一個(gè)線程安全類,它的add()、get()、size()等都是被synchronized修飾的,但這并不能保證它是絕對(duì)安全的。
如下代碼:
public class Test { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){ for(int index = 0;index < 10;index++){ vector.add(index); } //移除元素的線程 Thread removeThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i<vector.size(); i++){ vector.remove(i); } } }); //打印元素的線程 Thread printThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i<vector.size(); i++){ System.out.println(vector.get(i)); } } }); removeThread.start(); printThread.start(); //別創(chuàng)建太多線程,出現(xiàn)異常就手動(dòng)停止運(yùn)行吧,不然會(huì)一直執(zhí)行下去。 while (Thread.activeCount()>5); } } }
運(yùn)行結(jié)果:
Exception in thread "Thread-229" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 15 at java.util.Vector.get(Vector.java:748) at com.eurekaclient2.client2.shejimoshi.JVM.Test$2.run(Test.java:38) at java.lang.Thread.run(Thread.java:748)
盡管Vector的方法都是同步的,但是在多線程環(huán)境下,若不在調(diào)用方法端做額外的同步措施的話,仍然不是線程安全的,因?yàn)槿袅硪痪€程恰好在錯(cuò)誤的時(shí)間里刪除了一個(gè)元素,導(dǎo)致序號(hào)i已經(jīng)不再可用的話,再用i訪問數(shù)組就會(huì)拋出一個(gè)ArrayIndexOutOfBoundsException。
解決方法如下(將移除和打印都設(shè)置為同步):
public class Test { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){ for(int index = 0;index < 10;index++){ vector.add(index); } //移除元素的線程 Thread removeThread = new Thread(new Runnable() { @Override public void run() { synchronized(vector){ for (int i = 0; i<vector.size(); i++){ vector.remove(i); } } } }); //打印元素的線程 Thread printThread = new Thread(new Runnable() { @Override public void run() { synchronized(vector){ for (int i = 0; i<vector.size(); i++){ System.out.println(vector.get(i)); } } } }); removeThread.start(); printThread.start(); while (Thread.activeCount()>5); } } }
相對(duì)線程安全
我們通常所講的線程安全就是指的相對(duì)線程安全,需要保證對(duì)象單獨(dú)的操作時(shí)線程安全的,不需要做額外的保障措施。但若是對(duì)于一些特定屬性的連續(xù)調(diào)用,就可能會(huì)需要在調(diào)用端添加額外的同步措施。Java語(yǔ)言中,大部分的線程安全類都是相對(duì)線程安全的,例如Vector、HashTable以及Collections的synchronizedCollection()方法包裝的集合等。
線程兼容
線程兼容是指對(duì)象本身不是線程安全的,但是可以通過(guò)在調(diào)用端使用同步措施來(lái)保證對(duì)象在并發(fā)環(huán)境中可以安全的使用。Java中大部分類都是線程兼容的,如ArrayList、HashMap等等。
線程對(duì)立
線程對(duì)立指無(wú)論調(diào)用端是否采用了同步措施,都無(wú)法在多線程環(huán)境中并發(fā)使用代碼。這種代碼是有害的,應(yīng)盡量避免。常見的線程對(duì)立操作有System.setIn()、System.setOut()和System.runFinalizersOnExit()等。
線程安全的實(shí)現(xiàn)方法
線程安全的實(shí)現(xiàn)主要有以下幾個(gè)方法:
互斥同步
通過(guò)互斥來(lái)實(shí)現(xiàn)同步,臨界區(qū)、互斥量、信號(hào)量都是主要的互斥實(shí)現(xiàn)方法。在Java中最基本的互斥同步手段就是synchronized關(guān)鍵字,synchronized關(guān)鍵字通過(guò)編譯后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼都需要一個(gè)reference類型的慘呼是來(lái)指明要鎖定和解鎖的對(duì)象。若在程序中為synchronized指明了對(duì)象參數(shù),那就是這個(gè)對(duì)象的reference,若沒有指明,則根據(jù)synchronized修飾的是實(shí)例方法或類方法,來(lái)獲取對(duì)應(yīng)的對(duì)象或Class對(duì)象來(lái)作為鎖對(duì)象。
在虛擬機(jī)規(guī)范中要求,在執(zhí)行monitorernter指令時(shí),首先要嘗試獲取對(duì)象的鎖。如若此對(duì)象沒被鎖定或當(dāng)期線程已經(jīng)擁有了此對(duì)象的鎖,則把鎖的計(jì)數(shù)器加1,響應(yīng)的在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí),鎖就會(huì)被釋放。若獲取鎖失敗,那么當(dāng)前線程就要進(jìn)入阻塞狀態(tài),直到對(duì)象鎖被另外一個(gè)線程釋放為止。
有兩點(diǎn)需要注意的是:
- synchronized同步快對(duì)于同一條線程來(lái)說(shuō)是可重入的,不會(huì)出現(xiàn)自己把自己鎖死的問題。
- 同步塊在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其他線程的進(jìn)入。
除了synchronized之外,還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來(lái)實(shí)現(xiàn)同步。用法很相似,只是代碼寫法上有區(qū)別,ReentrantLock表現(xiàn)為API層面的互斥鎖(lock()和unlock()方法配合try/finally()語(yǔ)句塊來(lái)完成),synchronized表現(xiàn)為原生語(yǔ)法層面的互斥鎖。不過(guò)ReentrantLock比synchronized增加了一些高級(jí)功能,主要有以下3項(xiàng):等待可中斷、可實(shí)現(xiàn)公平鎖,以及鎖可以綁定多個(gè)條件。
- 等待可中斷是指當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對(duì)處理執(zhí)行時(shí)間非常長(zhǎng)的同步塊很有幫助。
- 公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖;非公平鎖,在釋放時(shí)任何一個(gè)等待線程都有機(jī)會(huì)獲得鎖。synchronized是非公平鎖,ReentrantLock默認(rèn)情況下也是非公平的,但可以通過(guò)帶布爾值的構(gòu)造函數(shù)要求使用公平鎖。
- 鎖綁定多個(gè)條件是指一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象,而synchronized中,鎖對(duì)象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件,如果要和多于一個(gè)的條件關(guān)聯(lián)的時(shí)候,就不得不額外的添加一個(gè)鎖,而ReentrantLock則無(wú)須這樣做,只需要多次調(diào)用newCondition()方法即可。
非阻塞同步
互斥同步最主要的問題就是進(jìn)行現(xiàn)場(chǎng)阻塞和喚醒鎖帶來(lái)的性能問題,因此這種同步也稱為阻塞同步(Block Synchronization)。從處理問題的方式上來(lái)說(shuō),互斥同步屬于一種悲觀的并發(fā)策略,那么相對(duì)而言的就有了另一種基于沖突檢測(cè)的樂觀并發(fā)策略,通俗的解釋就是先執(zhí)行操作,如果沒有其他線程爭(zhēng)用共享數(shù)據(jù),那操作就成功了;如果有線程爭(zhēng)用共享數(shù)據(jù),那就再采取其他補(bǔ)償措施(常見的補(bǔ)償措施就是不斷重試,直到成功為止),這種樂觀的并發(fā)策略不需要把線程掛起,因此也被稱為非阻塞同步(Non-Block Synchronization)。
在進(jìn)行操作和沖突檢測(cè)時(shí),需要保證這兩個(gè)步驟的原子性,這個(gè)時(shí)候如果靠同步互斥,那就也成悲觀并發(fā)了,所以只能靠硬件來(lái)完成這個(gè)保證,硬件保證一個(gè)從語(yǔ)義上開起來(lái)需要多次操作的行為只通過(guò)一條處理器指令就能完成,此類指令常用的有:
- 測(cè)試并設(shè)置(Test-and-Set)。
- 獲取并增加(Fetch-and-Increment)。
- 交換(Swap)。
- 比較并交換(Compare-and-Swap)CAS。
- 加載鏈接/條件存儲(chǔ)(Load-Linked/Store-COnditional)。
無(wú)同步方案
可重入代碼
如果一個(gè)方法,它的返回結(jié)果是可以預(yù)測(cè)的,只要輸入了相同的數(shù)據(jù),就都能返回相同的結(jié)果,那它就滿足可重入性的要求,當(dāng)然也就是線程安全的。這個(gè)方法就是可重入代碼,在這段代碼可以在執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼,而在控制權(quán)返回后,原來(lái)的程序不會(huì)出現(xiàn)任何錯(cuò)誤。
線程本地存儲(chǔ)
如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個(gè)線程中執(zhí)行?如果能保證,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個(gè)線程之內(nèi),這樣,無(wú)須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問題。例如大部分的消息隊(duì)列的架構(gòu)模式(生產(chǎn)者-消費(fèi)者)都符合這個(gè)特點(diǎn)。
作者:紀(jì)莫
歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處。
限于本人水平,如果文章和代碼有表述不當(dāng)之處,還請(qǐng)不吝賜教。
歡迎掃描二維碼關(guān)注公眾號(hào):Jimoer
文章會(huì)同步到公眾號(hào)上面,大家一起成長(zhǎng),共同提升技術(shù)能力。
聲援博主:如果您覺得文章對(duì)您有幫助,可以點(diǎn)擊文章右下角【推薦】一下。
您的鼓勵(lì)是博主的最大動(dòng)力!


浙公網(wǎng)安備 33010602011771號(hào)