聊聊圖解多線程
進(jìn)程與線程
「進(jìn)程」
進(jìn)程的本質(zhì)是一個(gè)正在執(zhí)行的程序,程序運(yùn)行時(shí)系統(tǒng)會(huì)創(chuàng)建一個(gè)進(jìn)程,并且「給每個(gè)進(jìn)程分配獨(dú)立的內(nèi)存地址空間,用來(lái)保證每個(gè)進(jìn)程地址不會(huì)相互干擾」。
同時(shí),在 CPU 對(duì)進(jìn)程做時(shí)間片的切換時(shí),保證進(jìn)程切換過(guò)程中仍然要從進(jìn)程切換之前運(yùn)行的位置處開始執(zhí)行。所以進(jìn)程通常還會(huì)包括程序計(jì)數(shù)器、堆棧指針。
相對(duì)好理解點(diǎn)的案例:電腦上開啟QQ就是開啟一個(gè)進(jìn)程、打開IDEA就是開啟一個(gè)進(jìn)程、打開瀏覽器就是開啟一個(gè)進(jìn)程.....
當(dāng)我們的電腦開啟你太多的運(yùn)用(QQ,微信,瀏覽器、PDF、word、IDEA等)后,電腦就很容易出現(xiàn)卡頓,甚至死機(jī),這里最主要原因就是CPU一直不停地切換導(dǎo)致的。
下圖是單核CPU情況下,多進(jìn)程之間的切換:
有了進(jìn)程以后,可以讓操作系統(tǒng)從宏觀層面實(shí)現(xiàn)多應(yīng)用并發(fā)。
而并發(fā)的實(shí)現(xiàn)是通過(guò) CPU 時(shí)間片不端切換執(zhí)行的,對(duì)于單核 CPU來(lái)說(shuō),在任意一個(gè)時(shí)刻只會(huì)有一個(gè)進(jìn)程在被CPU 調(diào)度。
線程的生命周期
既然是生命周期,那么就很有可能會(huì)有階段性的或者狀態(tài)的,比如人的一生一樣:
精子和卵子結(jié)合---> 嬰兒---> 小孩--> 成年--> 中年--> 老年-->去世
線程狀態(tài)
關(guān)于線程的生命周期網(wǎng)上有不一樣的答案,有說(shuō)五種也有說(shuō)六種。
Java中線程確實(shí)有6種,這是有理有據(jù)的,可以看看java.lang.Thread類中有個(gè)這么一個(gè)枚舉。
- public enum State {
 - NEW,
 - RUNNABLE,
 - BLOCKED,
 - WAITING,
 - TIMED_WAITING,
 - TERMINATED;
 - }
 
這就是Java線程對(duì)應(yīng)的狀態(tài),組合起來(lái)就是Java中一個(gè)線程的生命周期。下面是這個(gè)枚舉的注釋:
每種狀態(tài)簡(jiǎn)單說(shuō)明:
- NEW(初始):線程被創(chuàng)建后尚未啟動(dòng)。
 - RUNNABLE(運(yùn)行):包括了操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是處于此狀態(tài)的線程可能正在運(yùn)行,也可能正在等待系統(tǒng)資源,如等待CPU為它分配時(shí)間片。
 - BLOCKED(阻塞):線程阻塞于鎖。
 - WAITING(等待):線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷)。
 - TIME_WAITING(超時(shí)等待):該狀態(tài)不同于WAITING,它可以在指定的時(shí)間內(nèi)自行返回。
 - TERMINATED(終止):該線程已經(jīng)執(zhí)行完畢。
 
線程生命周期
借用網(wǎng)上的這張圖,這張圖描述的很清楚了,這里就不在啰嗦。
何為線程安全?
我們經(jīng)常會(huì)聽說(shuō)某個(gè)類是線程安全,某個(gè)類不是線程安全的。那么究竟什么叫做線程安全呢?
我們引用《Java Concurrency in Practice》里面的定義:
在不使用額外同步的情況下,多個(gè)線程訪問(wèn)一個(gè)對(duì)象時(shí),不論線程之間如何交替執(zhí)行或者在調(diào)用方進(jìn)行任何其它的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都能得到正確的結(jié)果,那么這個(gè)對(duì)象是線程安全的。
也可以這么理解:
多個(gè)線程訪問(wèn)同一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果,那么這個(gè)對(duì)象就是線程安全的?;蛘哒f(shuō):一個(gè)類或者程序所提供的接口對(duì)于線程來(lái)說(shuō)是原子操作或者多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性,也就是說(shuō)我們不用考慮同步的問(wèn)題。
可以簡(jiǎn)單的理解為:“你隨便怎么調(diào)用,出了問(wèn)題算我輸”。
這個(gè)定義對(duì)于類來(lái)說(shuō)是十分嚴(yán)格的,即使是Java API中標(biāo)為線程安全的類也很難滿足這個(gè)要求。
比如Vector是標(biāo)記為線程安全的,但實(shí)際上并不能滿足這個(gè)條件,舉個(gè)例子:
- public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
 - public synchronized E get(int index) {
 - if (index >= elementCount)
 - throw new ArrayIndexOutOfBoundsException(index);
 - return elementData(index);
 - }
 - public synchronized void removeElementAt(int index) {
 - modCount++;
 - if (index >= elementCount) {
 - throw new ArrayIndexOutOfBoundsException(index + " >= " +
 - elementCount);
 - }
 - else if (index < 0) {
 - throw new ArrayIndexOutOfBoundsException(index);
 - }
 - int j = elementCount - index - 1;
 - if (j > 0) {
 - System.arraycopy(elementData, index + 1, elementData, index, j);
 - }
 - elementCount--;
 - elementData[elementCount] = null; /* to let gc do its work */
 - }
 - //....基本上所有方法都是synchronized修飾的
 - }
 
來(lái)看下面一個(gè)案例:
判斷Vector中第0個(gè)元素是不是空字符,如果是空字符就將其刪除。
- package com.java.tian.blog.utils;
 - import java.util.Vector;
 - public class SynchronizedDemo{
 - static Vector<String> vct = new Vector<String>();
 - public void remove() {
 - if("".equals(vct.get(0))) {
 - vct.remove(0);
 - }
 - }
 - public static void main(String[] args) {
 - vct.add("");
 - SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
 - new Thread(new Runnable() {
 - @Override
 - public void run() {
 - synchronizedDemo.remove();
 - }
 - },"線程1").start();
 - new Thread(new Runnable() {
 - @Override
 - public void run() {
 - synchronizedDemo.remove();
 - }
 - },"線程2").start();
 - }
 - }
 
上面的邏輯看起來(lái)沒(méi)有問(wèn)題,實(shí)際上是有可能導(dǎo)致錯(cuò)誤的:假設(shè)第0個(gè)元素是空字符,判斷的時(shí)候得到的結(jié)果是true。
兩個(gè)線程同時(shí)執(zhí)行上面的remove方法,(「極端的情況」)都「可能」get到的是"",然后都去刪除第0個(gè)元素,這個(gè)元素有可能已經(jīng)被其它線程刪除了,因此Vector不是絕對(duì)線程安全的。(上面這個(gè)案例只是做演示而已,在你的業(yè)務(wù)代碼里面這么寫的話,線程安全真的就不能靠Vector來(lái)保證了)。
通常情況下我們說(shuō)的線程安全都是相對(duì)線程安全,相對(duì)線程安全只要求調(diào)用單個(gè)方法的時(shí)候不需要同步就可以得到正確的結(jié)果,但數(shù)多個(gè)方法組合調(diào)用的時(shí)候也是有可能導(dǎo)致多線程問(wèn)題的。
如果想讓上面的操作執(zhí)行正確,我們需要在調(diào)用Vector方法的時(shí)候添加額外的同步操作:
- package com.java.tian.blog.utils;
 - import java.util.Vector;
 - public class SynchronizedDemo {
 - static Vector<String> vct = new Vector<String>();
 - public void remove() {
 - synchronized (vct) {
 - //synchronized (SynchronizedDemo.class) {
 - if ("".equals(vct.get(0))) {
 - vct.remove(0);
 - }
 - }
 - }
 - public static void main(String[] args) {
 - vct.add("");
 - SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
 - new Thread(new Runnable() {
 - @Override
 - public void run() {
 - synchronizedDemo.remove();
 - }
 - }, "線程1").start();
 - new Thread(new Runnable() {
 - @Override
 - public void run() {
 - synchronizedDemo.remove();
 - }
 - }, "線程2").start();
 - }
 - }
 
根據(jù)Vector的源代碼可知:Vector的每個(gè)方法都使用了synchronized關(guān)鍵字修飾,因此鎖對(duì)象就是這個(gè)對(duì)象本身。在上面的代碼中我們嘗試獲取的也是vct對(duì)象的鎖,可以和vct對(duì)象的其它方法互斥,因此這樣做可以保證得到正確的結(jié)果。
如果Vector內(nèi)部使用的是其它鎖同步的,并封裝了鎖對(duì)象,那么我們無(wú)論如何都無(wú)法正確執(zhí)行這個(gè)“先判斷后修改”的操作。
假設(shè)被封裝的對(duì)象鎖為obj,get()和remove()方法對(duì)應(yīng)的鎖都是obj,而整個(gè)操作過(guò)程獲取的是vct的鎖,一個(gè)線程調(diào)用get()方法成功后就釋放了obj的鎖,這時(shí)這個(gè)線程只持有vct的鎖,而其它線程可以獲得obj的鎖并搶先一步刪除了第0個(gè)元素。
Java為開發(fā)者提供了很多強(qiáng)大的工具類,這些工具類里面有的是線程安全的,有的不是線程安全的。在這里我們列舉幾個(gè)面試??嫉模?/p>
線程安全的類:Vector、Hashtable、StringBuffer
非線程安全的類:ArrayList、HashMap、StringBuilder
有人可能會(huì)反問(wèn):為什么Java不把所有的類都設(shè)計(jì)成線程安全的呢?這樣對(duì)于我們開發(fā)者來(lái)說(shuō)豈不是更爽嗎?我們就不用考慮什么線程安全問(wèn)題了。
事情都是具有兩面性的,獲得線程安全但是性能會(huì)有所下降,畢竟鎖的開銷是擺在那里的。線程不安全但是性能會(huì)有所提升,具體場(chǎng)景還得看業(yè)務(wù)更偏向于哪一個(gè)。
一個(gè)問(wèn)題引發(fā)的思考:
- public class SynchronizedDemo {
 - static int count;
 - public void incre() {
 - try {
 - //每個(gè)線程都睡一會(huì),模仿業(yè)務(wù)代碼
 - Thread.sleep(100 );
 - } catch (InterruptedException e) {
 - e.printStackTrace();
 - }
 - count++;
 - }
 - public static void main(String[] args) {
 - SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
 - for (int i = 0; i < 1000; i++) {
 - new Thread(new Runnable() {
 - @Override
 - public void run() {
 - synchronizedDemo.incre();
 - }
 - }).start();
 - }
 - try {
 - //讓主線程等待所有線程執(zhí)行完畢
 - Thread.sleep(2000L);
 - } catch (InterruptedException e) {
 - e.printStackTrace();
 - }
 - System.out.println(count);
 - }
 - }
 
上面這段代碼輸出的結(jié)果是不確定的,結(jié)果是小于等于1000。
1000線程都去對(duì)count進(jìn)行++操作。
對(duì)象內(nèi)存布局
對(duì)象在內(nèi)存中的存儲(chǔ)可以分為 3 塊區(qū)域,分別是對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
其中,對(duì)象頭包括兩部分內(nèi)容,一部分是對(duì)象本身的運(yùn)行時(shí)數(shù)據(jù),像 GC 分代年齡、哈希碼、鎖狀態(tài)標(biāo)識(shí)等等,官方稱之為“Mark Word”,如果忽略壓縮指針的影響,這部分?jǐn)?shù)據(jù)在 32 位和 64 位的虛擬機(jī)中分別占 32 位和 64 位。
但是對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,32 位或者 64 位都不一定能存的下,考慮到虛擬機(jī)的空間效率,這個(gè) Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,對(duì)象處于不同狀態(tài)的時(shí)候,對(duì)應(yīng)的 bit 表示的含義可能會(huì)不一樣,見下圖,以 32 位 Hot Spot 虛擬機(jī)為例:
從上圖中我們可以看出,如果對(duì)象處于未鎖定狀態(tài)(無(wú)鎖態(tài)),那么 Mark Word 的 25 位用于存儲(chǔ)對(duì)象的哈希碼,4 位用于存儲(chǔ)對(duì)象分代年齡,1 位固定為 0,兩位用于存儲(chǔ)鎖標(biāo)志位。
這個(gè)圖對(duì)于理解后面提到的輕量級(jí)鎖、偏向鎖是非常重要的,當(dāng)然我們現(xiàn)在可以先著重考慮對(duì)象處于重量級(jí)鎖狀態(tài)下的情況,也就是鎖標(biāo)志位為 10。同時(shí)我們看到,無(wú)鎖態(tài)和偏向鎖狀態(tài)下,2 位鎖標(biāo)志位都是“01”,留有 1 位表示是否可偏向,我們姑且叫它“偏向位”。
「注」:對(duì)象頭的另一部分則是類型指針,虛擬機(jī)可以通過(guò)這個(gè)指針來(lái)確認(rèn)該對(duì)象是哪個(gè)類的實(shí)例。但是我們要注意,并不是所有的虛擬機(jī)都必須以這種方式來(lái)確定對(duì)象的元數(shù)據(jù)信息。對(duì)象的訪問(wèn)定位一般有句柄和直接指針兩種,如果使用句柄的話,那么對(duì)象的元數(shù)據(jù)信息可以直接包含在句柄中(當(dāng)然也包括對(duì)象實(shí)例數(shù)據(jù)的地址信息),也就沒(méi)必要將這些元數(shù)據(jù)和實(shí)例數(shù)據(jù)存儲(chǔ)在一起了。至于實(shí)例數(shù)據(jù)和對(duì)齊填充,這里暫不做討論。
前面我們提到了,Java 中的每個(gè)對(duì)象都與一個(gè) monitor 相關(guān)聯(lián),當(dāng)鎖標(biāo)志位為 10 時(shí),除了 2bit 的標(biāo)志位,指向的就是 monitor 對(duì)象的地址(還是以 32 位虛擬機(jī)為例)。這里我們可以翻閱一下 OpenJDK 的源碼,如果我們需要下載openJDK的源碼:
找到。這里先看一下markOpp.hpp文件。該文件的相對(duì)路徑為:
- openjdk\hotspot\src\share\vm\oops
 
下圖是文件中的注釋部分:
我們可以看到,其中描述了 32 位和 64 位下 Mark World 的存儲(chǔ)狀態(tài)。也可以看到64位下,前25位是沒(méi)有使用的。
我們也可以看到 markOop.hpp 中定義的鎖狀態(tài)枚舉,對(duì)應(yīng)我們前面提到的無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖(膨脹鎖)、GC 標(biāo)記等:
- enum { locked_value = 0,//00 輕量級(jí)鎖
 - unlocked_value = 1,//01 無(wú)鎖
 - monitor_value = 2,//10 重量級(jí)鎖
 - marked_value = 3,//11 GC標(biāo)記
 - biased_lock_pattern = 5 //101 偏向鎖,1位偏向標(biāo)記和2位狀態(tài)標(biāo)記(01)
 - };
 
從注釋中,我們也可以看到對(duì)其的簡(jiǎn)要描述,后面會(huì)我們?cè)敿?xì)解釋:
這里我們的重心還是是重量級(jí)鎖,所以我們看看源碼中 monitor 對(duì)象是如何定義的,對(duì)應(yīng)的頭文件是 objectMonitor.hpp,文件路徑為:
- openjdk\hotspot\src\share\vm\runtime
 
我們來(lái)簡(jiǎn)單看一下這個(gè) objectMonitor.hpp 的定義:
- // initialize the monitor, exception the semaphore, all other fields
 - // are simple integers or pointers
 - ObjectMonitor() {
 - _header = NULL;
 - _count = 0;
 - _waiters = 0,//等待線程數(shù)
 - _recursions = 0;//重入次數(shù)
 - _object = NULL;
 - _owner = NULL;//持有鎖的線程(邏輯上,實(shí)際上除了THREAD,還可能是Lock Record)
 - _WaitSet = NULL;//線程wait之后會(huì)進(jìn)入該列表
 - _WaitSetLock = 0 ;
 - _Responsible = NULL ;
 - _succ = NULL ;
 - _cxq = NULL ;//等待獲取鎖的線程列表,和_EntryList配合使用
 - FreeNext = NULL ;
 - _EntryList = NULL ;//等待獲取鎖的線程列表,和_cxq配合使用
 - _SpinFreq = 0 ;
 - _SpinClock = 0 ;
 - OwnerIsThread = 0 ;//當(dāng)前持有者是否為THREAD類型,如果是輕量級(jí)鎖膨脹而來(lái),還沒(méi)有enter的話,
 - //_owner存儲(chǔ)的可能會(huì)是Lock Record
 - _previous_owner_tid = 0;
 - }
 
簡(jiǎn)單的說(shuō),當(dāng)多個(gè)線程競(jìng)爭(zhēng)訪問(wèn)同一段同步代碼塊時(shí),如果線程獲取到了 monitor,那么就會(huì)把 _owner 設(shè)置成當(dāng)前線程,如果是重入的話,_recursions 會(huì)加 1,如果獲取 monitor 失敗,則會(huì)進(jìn)入 _cxq隊(duì)列。
鎖被釋放時(shí),_cxq中的線程會(huì)被移動(dòng)到 _EntryList中,并且喚醒_EntryList 隊(duì)首線程。當(dāng)然,選取喚醒線程有幾個(gè)不同的策略(Knob_QMode),還是后面結(jié)合源碼解析。
「注」:_cxq和 _EntryList本質(zhì)上是ObjectWaiter 類型,它本質(zhì)上其實(shí)是一個(gè)雙向鏈表 (具有前后指針),只是在使用的時(shí)候不一定要當(dāng)做雙向鏈表使用,比如 _cxq 是當(dāng)做單向鏈表使用的,_EntryList是當(dāng)做雙向鏈表使用的。
什么場(chǎng)景會(huì)導(dǎo)致線程的上下文切換?
導(dǎo)致線程上下文切換的有兩種類型:
自發(fā)性上下文切換是指線程由 Java 程序調(diào)用導(dǎo)致切出,在多線程編程中,執(zhí)行調(diào)用上圖中的方法或關(guān)鍵字,常常就會(huì)引發(fā)自發(fā)性上下文切換。
非自發(fā)性上下文切換指線程由于調(diào)度器的原因被迫切出。常見的有:線程被分配的時(shí)間片用完,虛擬機(jī)垃圾回收導(dǎo)致或者執(zhí)行優(yōu)先級(jí)的問(wèn)題導(dǎo)致。
waity /notify
注意兩個(gè)隊(duì)列:
等待隊(duì)列:notifyAll/notify喚醒的就是等待隊(duì)列中的線程;
同步線程:就是競(jìng)爭(zhēng)鎖的所有線程,等待隊(duì)列中的線程被喚醒后進(jìn)入同步隊(duì)列。
sleep與wait的區(qū)別
sleep
- 讓當(dāng)前線程休眠指定時(shí)間。
 - 休眠時(shí)間的準(zhǔn)確性依賴于系統(tǒng)時(shí)鐘和CPU調(diào)度機(jī)制。
 - 不釋放已獲取的鎖資源,如果sleep方法在同步上下文中調(diào)用,那么其他線程是無(wú)法進(jìn)入到當(dāng)前同步塊或者同步方法中的。
 - 可通過(guò)調(diào)用interrupt()方法來(lái)喚醒休眠線程。
 - sleep是Thread里的方法
 
wait
- 讓當(dāng)前線程進(jìn)入等待狀態(tài),當(dāng)別的其他線程調(diào)用notify()或者notifyAll()方法時(shí),當(dāng)前線程進(jìn)入就緒狀態(tài)
 - wait方法必須在同步上下文中調(diào)用,例如:同步方法塊或者同步方法中,這也就意味著如果你想要調(diào)用wait方法,前提是必須獲取對(duì)象上的鎖資源
 - 當(dāng)wait方法調(diào)用時(shí),當(dāng)前線程將會(huì)釋放已獲取的對(duì)象鎖資源,并進(jìn)入等待隊(duì)列,其他線程就可以嘗試獲取對(duì)象上的鎖資源。
 - wait是Object中的方法
 
樂(lè)觀鎖、悲觀鎖、可重入鎖.....
作為一個(gè)Java開發(fā)多年的人來(lái)說(shuō),肯定多多少少熟悉一些鎖,或者聽過(guò)一些鎖。今天就來(lái)做一個(gè)鎖相關(guān)總結(jié)。
悲觀鎖和樂(lè)觀鎖
悲觀鎖
顧名思義,他就是很悲觀,把事情都想的最壞,是指該鎖只能被一個(gè)線程鎖持有,如果A線程獲取到鎖了,這時(shí)候線程B想獲取鎖只能排隊(duì)等待線程A釋放。
在數(shù)據(jù)庫(kù)中這樣操作:
- select user_name,user_pwd from t_user for update;
 
樂(lè)觀鎖
顧名思義,樂(lè)觀,人樂(lè)觀就是什么是都想得開,船到橋頭自然直。樂(lè)觀鎖就是我都覺(jué)得他們都沒(méi)有拿到鎖,只有我拿到鎖了,最后再去問(wèn)問(wèn)這個(gè)鎖真的是我獲取的嗎?是就把事情給干了。
典型的代表:CAS=Compare and Swap 先比較哈,資源是不是我之前看到的那個(gè),是那我就把他換成我的。不是就算了。
在Java中java.util.concurrent.atomic包下面的原子變量就是使用了樂(lè)觀鎖的一種實(shí)現(xiàn)方式CAS實(shí)現(xiàn)。
通常都是 使用version、時(shí)間戳等來(lái)比較是否已被其他線程修改過(guò)。
使用悲觀鎖還是使用樂(lè)觀鎖?
在樂(lè)觀鎖與悲觀鎖的選擇上面,主要看下兩者的區(qū)別以及適用場(chǎng)景就可以了。
「響應(yīng)效率」
如果需要非常高的響應(yīng)速度,建議采用樂(lè)觀鎖方案,成功就執(zhí)行,不成功就失敗,不需要等待其他并發(fā)去釋放鎖。樂(lè)觀鎖并未真正加鎖,效率高。一旦鎖的粒度掌握不好,更新失敗的概率就會(huì)比較高,容易發(fā)生業(yè)務(wù)失敗。
「沖突頻率」
如果沖突頻率非常高,建議采用悲觀鎖,保證成功率。沖突頻率大,選擇樂(lè)觀鎖會(huì)需要多次重試才能成功,代價(jià)比較大。「重試代價(jià)」
如果重試代價(jià)大,建議采用悲觀鎖。悲觀鎖依賴數(shù)據(jù)庫(kù)鎖,效率低。更新失敗的概率比較低。
樂(lè)觀鎖如果有人在你之前更新了,你的更新應(yīng)當(dāng)是被拒絕的,可以讓用戶從新操作。悲觀鎖則會(huì)等待前一個(gè)更新完成。這也是區(qū)別。
公平鎖和非公平鎖
公平鎖
顧名思義,是公平的,先來(lái)先得,F(xiàn)IFO;必須遵守排隊(duì)規(guī)則。不能僭越。多個(gè)線程按照申請(qǐng)鎖的順序去獲得鎖,線程會(huì)直接進(jìn)入隊(duì)列去排隊(duì),永遠(yuǎn)都是隊(duì)列的第一位才能得到鎖。
在ReentrantLock中默認(rèn)使用的非公平鎖,但是可以在構(gòu)建ReentrantLock實(shí)例時(shí)候指定為公平鎖。
- ReentrantLock fairSyncLock = new ReentrantLock(true);
 
假設(shè)線程 A 已經(jīng)持有了鎖,這時(shí)候線程 B 請(qǐng)求該鎖將會(huì)被掛起,當(dāng)線程 A 釋放鎖后,假如當(dāng)前有線程 C 也需要獲取該鎖,那么在公平鎖模式下,獲取鎖和釋放鎖的步驟為:
- 線程A獲取鎖--->線程A釋放鎖
 - 線程B獲取鎖--->線程B釋放鎖;
 - 線程C獲取鎖--->線程釋放鎖;
 
「優(yōu)點(diǎn)」
所有的線程都能得到資源,不會(huì)餓死在隊(duì)列中。
「缺點(diǎn)」
吞吐量會(huì)下降很多,隊(duì)列里面除了第一個(gè)線程,其他的線程都會(huì)阻塞,CPU喚醒阻塞線程的開銷會(huì)很大。
非公平鎖
顧名思義,老子才不管你們誰(shuí)先排隊(duì)的,也就是平時(shí)大家在生活中很討厭的。生活中排隊(duì)的很多,上車排隊(duì)、坐電梯排隊(duì)、超市結(jié)賬付款排隊(duì)等等。但是不是每個(gè)人都會(huì)遵守規(guī)則站著排隊(duì),這就對(duì)站著排隊(duì)的人來(lái)說(shuō)就不公平了。等搶不到后再去乖乖排隊(duì)。
多個(gè)線程去獲取鎖的時(shí)候,會(huì)直接去嘗試獲取,獲取不到,再去進(jìn)入等待隊(duì)列,如果能獲取到,就直接獲取到鎖。
上面說(shuō)過(guò)在ReentrantLock中默認(rèn)使用的非公平鎖,兩種方式:
- ReentrantLock fairSyncLock = new ReentrantLock(false);
 
或者:
- ReentrantLock fairSyncLock = new ReentrantLock();
 
都可以實(shí)現(xiàn)非公平鎖。
「優(yōu)點(diǎn)」
可以減少CPU喚醒線程的開銷,整體的吞吐效率會(huì)高點(diǎn),CPU也不必取喚醒所有線程,會(huì)減少喚起線程的數(shù)量。
「缺點(diǎn)」
大家可能也發(fā)現(xiàn)了,這樣可能導(dǎo)致隊(duì)列中間的線程一直獲取不到鎖或者長(zhǎng)時(shí)間獲取不到鎖,導(dǎo)致餓死。
獨(dú)享鎖和共享鎖
獨(dú)享鎖
獨(dú)享鎖也叫排他鎖/互斥鎖,是指該鎖一次只能被一個(gè)線程鎖持有。如果線程T對(duì)數(shù)據(jù)A加上排他鎖后,則其他線程不能再對(duì)A加任何類型的鎖。獲得排他鎖的線程既能讀數(shù)據(jù)又能修改數(shù)據(jù)。JDK中的synchronized和JUC中Lock的實(shí)現(xiàn)類就是互斥鎖。
共享鎖
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后,則其他線程只能對(duì)A再加共享鎖,不能加排他鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。
對(duì)于ReentrantLock而言,其是獨(dú)享鎖。但是對(duì)于Lock的另一個(gè)實(shí)現(xiàn)類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨(dú)享鎖。
- 讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫,寫讀 ,寫寫的過(guò)程是互斥的。
 - 獨(dú)享鎖與共享鎖也是通過(guò)AQS來(lái)實(shí)現(xiàn)的,通過(guò)實(shí)現(xiàn)不同的方法,來(lái)實(shí)現(xiàn)獨(dú)享或者共享。
 
可重入鎖
若當(dāng)前線程執(zhí)行中已經(jīng)獲取了鎖,如果再次獲取該鎖時(shí),就會(huì)獲取不到被阻塞。
- public class RentrantLockDemo {
 - public synchronized void test(){
 - System.out.println("test");
 - }
 - public synchronized void test1(){
 - System.out.println("test1");
 - test();
 - }
 - public static void main(String[] args) {
 - RentrantLockDemo rentrantLockDemo = new RentrantLockDemo();
 - //線程1
 - new Thread(() -> rentrantLockDemo.test1()).start();
 - }
 - }
 
當(dāng)一個(gè)線程執(zhí)行test1()方法的時(shí)候,需要獲取rentrantLockDemo的對(duì)象鎖,在test1方法匯總又會(huì)調(diào)用test方法,但是test()的調(diào)用是需要獲取對(duì)象鎖的。
可重入鎖也叫「遞歸鎖」,指的是同一線程外層函數(shù)獲得鎖之后,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響。
ThreadLocal設(shè)計(jì)原理
ThreadLocal名字中有個(gè)Thread表示線程,Local表示本地,我們就理解為線程本地變量了。
先看看ThreadLocal的整體:
最關(guān)心的三個(gè)公有方法:set、get、remove。
構(gòu)造方法
- public ThreadLocal() {
 - }
 
構(gòu)造方法里沒(méi)有任何邏輯處理,就是簡(jiǎn)單的創(chuàng)建一個(gè)實(shí)例。
set方法
源碼為:
- public void set(T value) {
 - //獲取當(dāng)前線程
 - Thread t = Thread.currentThread();
 - //這是什么鬼?
 - ThreadLocalMap map = getMap(t);
 - if (map != null)
 - map.set(this, value);
 - else
 - createMap(t, value);
 - }
 
先看看ThreadLocalMap是個(gè)什么東東:
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。
set方法整體為:
ThreadLocalMap構(gòu)造方法:
- //這個(gè)屬性是ThreadLocal的,就是獲取hashcode(這列很有學(xué)問(wèn),但是我們的目的不是他)
 - private final int threadLocalHashCode = nextHashCode();
 - private Entry[] table;
 - private static final int INITIAL_CAPACITY = 16;
 - //Entry是一個(gè)弱引用
 - static class Entry extends WeakReference<ThreadLocal<?>> {
 - Object value;
 - Entry(ThreadLocal<?> k, Object v) {
 - super(k);
 - value = v;
 - }
 - }
 - ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 - //數(shù)組默認(rèn)大小為16
 - table = new Entry[INITIAL_CAPACITY];
 - //len 為2的n次方,以ThreadLocal的計(jì)算的哈希值按照Entry[]取模(為了更好的散列)
 - int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 - table[i] = new Entry(firstKey, firstValue);
 - size = 1;
 - //設(shè)置閾值(擴(kuò)容閾值)
 - setThreshold(INITIAL_CAPACITY);
 - }
 
然后我們看看map.set()方法中是如何處理的:
- private void set(ThreadLocal<?> key, Object value) {
 - Entry[] tab = table;
 - int len = tab.length;
 - //len 為2的n次方,以ThreadLocal的計(jì)算的哈希值按照Entry[]取模
 - int i = key.threadLocalHashCode & (len-1);
 - //找到ThreadLocal對(duì)應(yīng)的存儲(chǔ)的下標(biāo),如果當(dāng)前槽內(nèi)Entry不為空,
 - //即當(dāng)前線程已經(jīng)有ThreadLocal已經(jīng)使用過(guò)Entry[i]
 - for (Entry e = tab[i];
 - e != null;
 - e = tab[i = nextIndex(i, len)]) {
 - ThreadLocal<?> k = e.get();
 - // 當(dāng)前占據(jù)該槽的就是當(dāng)前的ThreadLocal ,更新value結(jié)束
 - if (k == key) {
 - e.value = value;
 - return;
 - }
 - //當(dāng)前卡槽的弱引用可能會(huì)回收了,key:null value:xxxObject ,
 - //需清理Entry原來(lái)的value ,便于垃圾回收value,且將新的value 放在該槽里,結(jié)束
 - if (k == null) {
 - replaceStaleEntry(key, value, i);
 - return;
 - }
 - }
 - //在這之前沒(méi)有ThreadLocal使用Entry[i],并進(jìn)行值存儲(chǔ)
 - tab[i] = new Entry(key, value);
 - //累計(jì)Entry所占的個(gè)數(shù)
 - int sz = ++size;
 - // 清理key 為null 的Entry ,可能需要擴(kuò)容,擴(kuò)容長(zhǎng)度為原來(lái)的2倍,并需要進(jìn)行重新hash
 - if (!cleanSomeSlots(i, sz) && sz >= threshold){
 - rehash();
 - }
 
從上面這個(gè)set方法,我們就大致可以把這三個(gè)進(jìn)行一個(gè)關(guān)聯(lián)了:
Thread、ThreadLocal、ThreadLocalMap。
get方法
remove方法
expungeStaleEntry方法代碼里有點(diǎn)大,所以這里就貼了出來(lái)。
- //刪除陳舊entry的核心方法
 - private int expungeStaleEntry(int staleSlot) {
 - Entry[] tab = table;
 - int len = tab.length;
 - tab[staleSlot].value = null;//刪除value
 - tab[staleSlot] = null;//刪除entry
 - size--;//map的size自減
 - // 遍歷指定刪除節(jié)點(diǎn),所有后續(xù)節(jié)點(diǎn)
 - Entry e;
 - int i;
 - for (i = nextIndex(staleSlot, len);
 - (e = tab[i]) != null;
 - i = nextIndex(i, len)) {
 - ThreadLocal<?> k = e.get();
 - if (k == null) {//key為null,執(zhí)行刪除操作
 - e.value = null;
 - tab[i] = null;
 - size--;
 - } else {//key不為null,重新計(jì)算下標(biāo)
 - int h = k.threadLocalHashCode & (len - 1);
 - if (h != i) {//如果不在同一個(gè)位置
 - tab[i] = null;//把老位置的entry置null(刪除)
 - // 從h開始往后遍歷,一直到找到空為止,插入
 - while (tab[h] != null){
 - h = nextIndex(h, len);
 - }
 - tab[h] = e;
 - }
 - }
 - }
 - return i;
 - }
 
本文轉(zhuǎn)載自微信公眾號(hào)「Java后端技術(shù)全?!?,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java后端技術(shù)全棧公眾號(hào)。









































 
 
 










 
 
 
 