推薦課程
目錄
1.4、wait/notify/notifyAll注意事項(xiàng)
1.4.1、為什么 wait 、notify、notifyAll必須在 synchronized 保護(hù)的同步代碼中使用?
1.4.2、為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?
1.4.3、wait/notify 和 sleep 方法的異同?
1.5、實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式的方法
2.1.2、發(fā)布和初始化導(dǎo)致線程安全問(wèn)題
2.2、哪些場(chǎng)景需要注意線程安全問(wèn)題
2.3、為什么多線程會(huì)帶來(lái)性能問(wèn)題
3.6、為什么不要通過(guò)Executors創(chuàng)建線程池
4.9、JVM對(duì)synchronized的優(yōu)化
5.3、為什么Map桶中超過(guò)8個(gè)才轉(zhuǎn)為紅黑樹(shù)
5.4、ConcurrentHashMap 和 Hashtable 的區(qū)別
6.5.1、線程池對(duì)應(yīng)的阻塞隊(duì)列
7.2、如何解決AtomicInteger在高并發(fā)下的性能問(wèn)題
7.4、AtomicInteger 和 synchronized 的異同點(diǎn)?
7.5、Java 8 中 Adder 和 Accumulator 有什么區(qū)別?
8.2、ThreadLocal是用來(lái)解決共享資源的多線程訪問(wèn)問(wèn)題嗎?
8.3、多個(gè)ThreadLocal在Thread的threadlocals里是如何存儲(chǔ)的?
1、線程基礎(chǔ)
1.1、線程實(shí)現(xiàn)方法
- 實(shí)現(xiàn)Runnable接口(無(wú)返回值)
- 繼承Thread類
- 線程池創(chuàng)建線程
- 實(shí)現(xiàn)Callable接口(有返回值)
1.2、如何正確停止線程
- interrupt()方法:請(qǐng)求中斷,設(shè)置中斷標(biāo)志(推薦)
若線程處于正常狀態(tài),則會(huì)將該線程的中斷標(biāo)志設(shè)置為true,之后線程將繼續(xù)正常運(yùn)行,不受影響。
若線程處于被阻塞狀態(tài),或由正常狀態(tài)變?yōu)樽枞麪顟B(tài)(sleep()、wait()),線程將立刻退出被阻塞狀態(tài),并拋出一個(gè)InterruptedException異常??梢酝ㄟ^(guò)捕獲異常然后return。
- stop():停止線程(不推薦)
直接把線程停止,影響數(shù)據(jù)完整性
- suspend():暫停線程(不推薦)
暫停線程,但不會(huì)釋放鎖,可能會(huì)導(dǎo)致死鎖
- resume():恢復(fù)因suspend掛起的線程(不推薦)
- volatile變量(推薦)
在存在循環(huán)的線程run方法中進(jìn)行判斷是否退出線程
但在某些特殊的情況下,比如線程被長(zhǎng)時(shí)間阻塞的情況,就無(wú)法及時(shí)感受中斷,所以 volatile 是不夠全面的停止線程的方法。
1.3、Java線程的六種狀態(tài)
[圖片上傳失敗...(image-2358ea-1689324444603)]
- New(新建):已創(chuàng)建線程,但還未執(zhí)行start()
- Runnable(就緒):對(duì)應(yīng)操作系統(tǒng)的Running和Ready狀態(tài)(CPU分配時(shí)間片)
- Blocked(被阻塞):進(jìn)入 synchronized 保護(hù)的代碼時(shí)沒(méi)有搶到 monitor 鎖
- Waiting(等待):執(zhí)行沒(méi)有設(shè)置timeout參數(shù)的Object.wait()、Thread.join()方法
- Timed Waiting(計(jì)時(shí)等待):執(zhí)行設(shè)置了timeout參數(shù)的Object.wait()、Thread.join()、Thread.sleep()方法
- Terminated(終止):run()方法執(zhí)行完畢;或者出現(xiàn)未捕獲的異常,導(dǎo)致意外終止
注意:waiting和Timed Waiting兩種狀態(tài)的線程,在被其他線程使用notify/notifyAll喚醒時(shí),會(huì)先進(jìn)入到Blocked狀態(tài)。因?yàn)閱拘?Waiting 線程的線程如果調(diào)用 notify() 或 notifyAll(),要求必須首先持有該 monitor 鎖,所以處于 Waiting 狀態(tài)的線程被喚醒時(shí)拿不到該鎖,就會(huì)進(jìn)入 Blocked 狀態(tài),直到執(zhí)行了 notify()/notifyAll() 的喚醒它的線程執(zhí)行完畢并釋放 monitor 鎖,才可能輪到它去搶奪這把鎖,如果它能搶到,就會(huì)從 Blocked 狀態(tài)回到 Runnable 狀態(tài)。
1.4、wait/notify/notifyAll注意事項(xiàng)
1.4.1、為什么 wait 、notify、notifyAll必須在 synchronized 保護(hù)的同步代碼中使用?
預(yù)防饑餓線程:在以下消費(fèi)者-生產(chǎn)者模式代碼中,生產(chǎn)者執(zhí)行g(shù)ive方法,消費(fèi)者執(zhí)行take方法。如果執(zhí)行順序如下所示,且沒(méi)有其他生產(chǎn)者,那么可能導(dǎo)致消費(fèi)者線程長(zhǎng)久處于wait()狀態(tài)。
[圖片上傳失敗...(image-ea3ef8-1689324444603)]
1.4.2、為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?
每個(gè)對(duì)象都有一個(gè)monitor鎖,鎖是對(duì)象級(jí)別的,而非線程級(jí)別的操作。wait、notify、notifyAll都是鎖級(jí)別的操作,所以將這三個(gè)方法定義在Object類中(Object是所有對(duì)象的父類)
而sleep的作用是休眠線程,是線程級(jí)別的,不會(huì)操作對(duì)象鎖,所以直接放在Thread類中。
1.4.3、wait/notify 和 sleep 方法的異同?
相同點(diǎn):
都會(huì)阻塞線程、都可以響應(yīng)interrupt中斷--->拋出異常
不同點(diǎn):
- wait/notify必須放在synchronized中,sleep不用;
- wait阻塞線程會(huì)釋放鎖,sleep不會(huì)釋放鎖;
- wait的線程必須等待notify喚醒,而sleep可設(shè)置休眠時(shí)間恢復(fù)自動(dòng)喚醒線程;
- wait/notify是Object類的方法,sleep是Thread類方法。
1.5、實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式的方法
1.5.1、BlockingQueue
public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue<>(10);
Runnable producer = () -> {
while (true) {
queue.put(new Object());
}
};
new Thread(producer).start();
new Thread(producer).start();
Runnable consumer = () -> {
while (true) {
queue.take();
}
};
new Thread(consumer).start();
new Thread(consumer).start();
}
1.5.2、Condition
public class MyBlockingQueueForCondition {
private Queue queue;
private int max = 16;
private ReentrantLock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public MyBlockingQueueForCondition(int size) {
this.max = size;
queue = new LinkedList();
}
public void put(Object o) throws InterruptedException {
lock.lock();
try {
while (queue.size() == max) {
notFull.await();
}
queue.add(o);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (queue.size() == 0) {
notEmpty.await();
}
Object item = queue.remove();
notFull.signalAll();
return item;
} finally {
lock.unlock();
}
}
}
1.5.3、 wait/notify
class MyBlockingQueue {
private int maxSize;
private LinkedList storage;
public MyBlockingQueue(int size) {
this.maxSize = size;
storage = new LinkedList<>();
}
public synchronized void put() throws InterruptedException {
while (storage.size() == maxSize) {
wait();
}
storage.add(new Object());
notifyAll();
}
public synchronized void take() throws InterruptedException {
while (storage.size() == 0) {
wait();
}
System.out.println(storage.remove());
notifyAll();
}
}
2、線程安全
2.1、3種線程安全問(wèn)題
2.1.1、運(yùn)行結(jié)果問(wèn)題
多線程情況下,多次執(zhí)行同一段代碼,運(yùn)行結(jié)果不同。
2.1.2、發(fā)布和初始化導(dǎo)致線程安全問(wèn)題
新建一個(gè)線程用于對(duì)象初始化,在初始化操作完成之前,另一個(gè)線程使用該對(duì)象,對(duì)象的屬性可能就不是預(yù)期的初始化后的值。
2.1.3、活躍性問(wèn)題
- 死鎖:兩個(gè)線程互相持有對(duì)方需要的資源(鎖),但又互不相讓。
- 活鎖:線程未阻塞,但線程運(yùn)行異常,報(bào)錯(cuò)時(shí)無(wú)限重試。導(dǎo)致線程處于活躍狀態(tài)卻永遠(yuǎn)得不到結(jié)果。
- 饑餓:一個(gè)線程長(zhǎng)時(shí)間在等待資源:比如1個(gè)低優(yōu)先級(jí)的線程,和1000000個(gè)高優(yōu)先級(jí)線程(不斷生產(chǎn)),此時(shí)這個(gè)低優(yōu)先級(jí)線程就可能沒(méi)機(jī)會(huì)運(yùn)行,導(dǎo)致饑餓。
2.2、哪些場(chǎng)景需要注意線程安全問(wèn)題
- 訪問(wèn)共享變量或資源
- 多線程下依賴時(shí)序的操作
if (map.containsKey(key)) {
map.remove(obj)
}
- 對(duì)方?jīng)]有聲明自己是線程安全的(比如ArrayList)
2.3、為什么多線程會(huì)帶來(lái)性能問(wèn)題
- 上下文切換:上下文切換會(huì)掛起當(dāng)前正在執(zhí)行的線程并保存當(dāng)前的狀態(tài),然后尋找下一處即將恢復(fù)執(zhí)行的代碼,喚醒下一個(gè)線程,以此類推,反復(fù)執(zhí)行
- 緩存失效:線程切換后,CPU高速緩存會(huì)失效
- 多線程間協(xié)作開(kāi)銷大
3、線程池
3.1、線程池的好處
- 池中有固定線程,復(fù)用線程,避免頻繁創(chuàng)建銷毀線程,減少線程生命周期的開(kāi)銷,提高性能
- 根據(jù)配置和任務(wù)數(shù)量控制線程數(shù)量,避免線程過(guò)多導(dǎo)致資源浪費(fèi)
- 統(tǒng)一管理池中的線程
3.2、創(chuàng)建線程池各參數(shù)的含義
[圖片上傳失敗...(image-5ce32d-1689324444603)]
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
->
new ThreadPoolExecutor(核心線程數(shù),最大線程數(shù),非核心線程存活時(shí)間,時(shí)間單位,緩沖隊(duì)列,創(chuàng)建線程使用的工廠,任務(wù)拒絕策略)。
3.3、線程池執(zhí)行流程
new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit unit, BlockingQueue
核心線程數(shù)->緩沖隊(duì)列->最大線程數(shù)->拒絕策略。
[圖片上傳失敗...(image-1cae81-1689324444603)]
流程:
- 核心線程數(shù)未滿時(shí),每次來(lái)任務(wù)就會(huì)創(chuàng)建新的核心線程來(lái)執(zhí)行任務(wù);
- 核心線程滿了,隊(duì)列未滿,則將任務(wù)放到隊(duì)列中;
- 核心線程滿了,隊(duì)列滿了,線程未達(dá)到最大線程數(shù),創(chuàng)建非核心線程數(shù)執(zhí)行線程;
- 核心線程滿了,隊(duì)列滿了,線程已達(dá)到最大線程數(shù),此時(shí)線程池工作飽和,執(zhí)行線程池的拒絕策略。
3.4、默認(rèn)四種拒絕策略
3.4.1、拒絕時(shí)機(jī)
- 調(diào)用shutdown等方法關(guān)閉線程池后,正在執(zhí)行線程池內(nèi)剩余的線程,此時(shí)再向線程池提交任務(wù),就會(huì)遭到拒絕。
- 線程池已經(jīng)沒(méi)有能力處理新的任務(wù):隊(duì)列已滿、線程池已滿。
3.4.2、拒絕策略
- AbortPolicy:直接拋出異常
- DiscardPolicy:直接丟棄新來(lái)的任務(wù),用戶無(wú)法感知
- DiscardOldestPolicy:丟棄隊(duì)列中存活時(shí)間最長(zhǎng)的任務(wù),用戶無(wú)法感知
- CallerRunsPolicy:把新來(lái)的任務(wù)直接交個(gè)調(diào)用者線程運(yùn)行
3.4、常見(jiàn)線程池
FixedThreadPool:固定線程池:核心線程數(shù) = 最大線程數(shù)
CachedThreadPool:可緩存線程池:線程數(shù)可以無(wú)限增加,并可對(duì)閑置線程進(jìn)行回收
ScheduledThreadPool:定時(shí)/周期性線程池:支持定時(shí)或周期性執(zhí)行任務(wù)(多數(shù)情況下可用Timer類代替)
SingleThreadExecutor:?jiǎn)尉€程線程池
3.5、ForkJoinPool
3.5.1、功能
將一個(gè)大任務(wù)拆分成多個(gè)小任務(wù)后,使用fork可以將小任務(wù)分發(fā)給其他線程同時(shí)處理,使用join可以將多個(gè)線程處理的結(jié)果進(jìn)行匯總
3.5.2、父類
RecursiveAction:用于沒(méi)有返回結(jié)果的任務(wù)(類似Runnable)
RecursiveTask:用于有返回結(jié)果的任務(wù)(類似Callable)
3.5.3、自定義Task類
class CountTask extends RecursiveTask {
private static final int THREADHOLD = 2;
private int start;
private int end;
/**
* @Description: 最小的一個(gè)任務(wù)單元,以及它要處理的范圍
* @param:
* @return:
*/
public CountTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end - start)
3.5.4、創(chuàng)建線程池
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CountTask sumTask1 = new CountTask(0, 4);
ForkJoinTask task1 = forkJoinPool.submit(sumTask1);
System.out.println(task1.get());
}
3.5.5、任務(wù)運(yùn)行原理
[圖片上傳失敗...(image-b4e29f-1689324444603)]
3.6、線程池常用的阻塞隊(duì)列
LinkedBlockingQueue:無(wú)界隊(duì)列
核心線程數(shù)固定的線程池,需要一個(gè)沒(méi)有容量限制的阻塞隊(duì)列。
FixedThreadPool、SingleThreadExector
SynchronousQueue
容量為0,一旦有任務(wù)進(jìn)入隊(duì)列,就立刻將任務(wù)提交給線程池,所以需要線程池有足夠的線程數(shù)
CachedThreadPool
DelayedWorkQueue
該隊(duì)列把任務(wù)按照延時(shí)時(shí)間長(zhǎng)短進(jìn)行排序,方便任務(wù)進(jìn)行
ScheduledThreadPool
3.6、為什么不要通過(guò)Executors創(chuàng)建線程池
- 內(nèi)存溢出:newFixedThreadPool、newSingleThreadExecutor
- OOM:newCachedThreadPool
- 通過(guò)使用new ThreadPoolExecutor()手動(dòng)創(chuàng)建線程池,根據(jù)實(shí)際情況選擇合適的隊(duì)列、拒絕策略、線程數(shù)等,避免資源耗盡的風(fēng)險(xiǎn)。
3.7、合適的線程數(shù)量
目的:合適的線程數(shù)量可以更好地使用CPU和內(nèi)存等資源,從而最大限度地提升程序的性能。
3.7.1、CPU密集型任務(wù):
- 線程數(shù) = CPU核心數(shù)的1~2倍
- 線程數(shù) = CPU 核心數(shù) *(1+平均等待時(shí)間/平均工作時(shí)間)
計(jì)算任務(wù)重,單個(gè)任務(wù)耗時(shí)長(zhǎng),如果線程數(shù)過(guò)多,會(huì)造成不必要的上下文切換,降低系統(tǒng)性能。
3.7.2、耗時(shí)IO型任務(wù):
線程數(shù) = CPU 核心數(shù) *(1+平均等待時(shí)間/平均工作時(shí)間)
IO 讀寫速度相比于 CPU 的速度而言是比較慢的,如果我們?cè)O(shè)置過(guò)少的線程數(shù),就可能導(dǎo)致 CPU 資源的浪費(fèi)。而如果我們?cè)O(shè)置更多的線程數(shù),那么當(dāng)一部分線程正在等待 IO 的時(shí)候,它們此時(shí)并不需要 CPU 來(lái)計(jì)算,那么另外的線程便可以利用 CPU 去執(zhí)行其他的任務(wù),互不影響,這樣的話在任務(wù)隊(duì)列中等待的任務(wù)就會(huì)減少,可以更好地利用資源。
3.8、如何正確關(guān)閉線程池
- *shutdown()-推薦
拒絕新任務(wù)的提交,執(zhí)行完當(dāng)前線程池中正在執(zhí)行的任務(wù)及隊(duì)列中的任務(wù)后,關(guān)閉線程池。
- isShutdown()
判斷線程池是否已經(jīng)開(kāi)始了關(guān)閉工作;
當(dāng)調(diào)用shutdown()或shutdownNow()方法后返回為true;
為true僅表示關(guān)閉流程開(kāi)始,不一定已經(jīng)關(guān)閉。
- isTerminated()
線程池已關(guān)閉,且池中和隊(duì)列中所有線程已執(zhí)行完畢時(shí),返回true
- awaitTermination()
線程等待一段指定的時(shí)間,如果在等待時(shí)間內(nèi),線程池已關(guān)閉并且內(nèi)部的任務(wù)都執(zhí)行完畢了,那么方法就返回 true,否則超時(shí)返回 fasle。
- *shutdownNow()-推薦
給所有線程發(fā)送中斷信號(hào),然后會(huì)將隊(duì)列中正在等待的所有任務(wù)轉(zhuǎn)移到一個(gè) List 中并返回,我們可以根據(jù)返回的任務(wù) List 來(lái)進(jìn)行一些補(bǔ)救的操作。
3.9、如何設(shè)計(jì)線程池
- 考慮線程池中的個(gè)數(shù)是固定的還是動(dòng)態(tài)變化的。
- 考慮隊(duì)列大小:無(wú)界隊(duì)列(可能導(dǎo)致內(nèi)存耗盡);有界隊(duì)列(隊(duì)列滿了拒絕策略)
- 每次提交新任務(wù),是直接放入隊(duì)列,還是開(kāi)啟新線程
- 沒(méi)有任務(wù)時(shí),線程是睡眠一段時(shí)間還是進(jìn)入阻塞
4、鎖
4.1、鎖分類
4.1.1、偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
特指synchronized鎖的狀態(tài)。在對(duì)象頭中的mark word來(lái)表明鎖的狀態(tài)。
偏向鎖:不加鎖,加標(biāo)記。
輕量級(jí)鎖:不阻塞,通過(guò)自旋和CAS獲取鎖。
重量級(jí)鎖:阻塞,互斥鎖。
鎖升級(jí):無(wú)鎖→偏向鎖→輕量級(jí)鎖→重量級(jí)鎖
4.1.2、可重入鎖/非可重復(fù)鎖
已經(jīng)持有鎖,是否能在不釋放當(dāng)前鎖的情況下,再次獲取當(dāng)前鎖。
4.1.3、共享鎖/獨(dú)占鎖
是否可被多個(gè)線程同時(shí)獲取鎖。典型為讀寫鎖。
4.1.4、公平鎖/非公平鎖
線程是否按照先來(lái)先到的原則獲取鎖。
4.1.5、悲觀鎖/樂(lè)觀鎖
悲觀鎖:獲取資源前,必須先獲得鎖,鎖住資源,synchronized。
樂(lè)觀鎖:獲取資源前,不用獲得鎖,不會(huì)鎖住資源,CAS。
適用場(chǎng)景:
悲觀鎖:并發(fā)寫入多、臨界區(qū)代碼復(fù)雜、競(jìng)爭(zhēng)激烈等場(chǎng)景。
樂(lè)觀鎖:大部分是讀取,少部分是修改的場(chǎng)景,也適合雖然讀寫都很多,但是并發(fā)并不激烈的場(chǎng)景。
4.1.6、自旋鎖/非自旋鎖
是否利用循環(huán),不停地嘗試獲取鎖。
4.1.7、可中斷鎖/不可中斷鎖
可中斷鎖:一旦申請(qǐng)鎖,就只能拿到鎖后才能做其他操作(synchronized)。
不可中斷鎖:線程申請(qǐng)鎖后,可中斷獲取鎖的流程(ReentrantLock)。
4.2、synchronized的monitor鎖
javac xxx.java:產(chǎn)生xxx.class
javap -verbose xxx.class:生成class文件的反匯編內(nèi)容
4.2.1、同步代碼塊
monitorenter(加鎖)、monitorexit(釋放鎖)指令:每個(gè)對(duì)象頭有一個(gè)記錄著鎖次數(shù)的計(jì)數(shù)器
4.2.2、同步方法
ACC_SYNCHRONIZED標(biāo)識(shí)符:如果方法有該標(biāo)識(shí)符,那么在調(diào)用方法時(shí)會(huì)先判斷獲取對(duì)象的monitor鎖,方法執(zhí)行之后釋放monitor鎖。
4.3、如何選擇synchronized和Lock
4.3.1、相同點(diǎn)
- 保證線程安全;
- 保證可見(jiàn)性(前一個(gè)加鎖操作結(jié)果對(duì)后一個(gè)加鎖操作可見(jiàn));
- 可重入鎖。
4.3.2、不同點(diǎn)
synchronized
- 自動(dòng)加鎖和釋放鎖。
- 加解鎖順序:加鎖Lock1->加鎖Lock2->解鎖Lock2->解鎖Lock1。
- 申請(qǐng)鎖后,只有獲得鎖后才能繼續(xù)其他操作,否則會(huì)永遠(yuǎn)等待。
- 只能被一個(gè)線程擁有。
- 不可設(shè)置公平鎖/非公平鎖。
- Java5之前,性能低;Java6之后,增加自旋鎖、輕量級(jí)鎖、偏向鎖,性能提高
Lock
- 必須顯示lock()和unlock()
- 加解鎖順序:可自由控制加解鎖順序
- 申請(qǐng)鎖后,可中斷退出。
- 可被多個(gè)線程擁有。
- 可設(shè)置公平鎖/非公平鎖。
4.3.3、如何選擇
- 最好的情況是兩者都不用,優(yōu)先使用并發(fā)工具包來(lái)加解鎖;
- 盡量使用synchronized由于lock,因?yàn)閟ynchronized可自動(dòng)加解鎖;
- 需要使用到嘗試獲取鎖、鎖中斷、超時(shí)功能時(shí),才使用Lock。
4.4、Lock的常用方法(ReentrantLock)
- lock()
獲取鎖,不能被中斷,死鎖->永久等待。必須在finally中unlock解鎖。
- tryLock()
嘗試獲取鎖,獲取成功,返回true;否則返回false。可根據(jù)是否獲得鎖決定后續(xù)程序的行為。
- tryLock(long time, TimeUnit unit)
與tryLock類似,區(qū)別在于獲取鎖失敗后會(huì)等待一段時(shí)間,一段時(shí)間后還獲取不到鎖,再返回false。
- lockInterruptibly()
獲取鎖,能獲取則直接返回,不能獲取則一直嘗試獲取鎖,可響應(yīng)中斷。
- unlock()
釋放鎖,必須寫在finally塊中。
4.5、公平鎖和非公平鎖
- 公平鎖:按照線程請(qǐng)求順序分配鎖。慢,吞吐量小,喚醒阻塞線程開(kāi)銷大。
- 非公平鎖:在一定情況下,允許插隊(duì),但不是隨意插隊(duì)??欤掏铝看?,有可能產(chǎn)生饑餓線程。
- 通過(guò)new ReentrantLock(false) 來(lái)設(shè)置公平鎖/非公平鎖。
4.5.1、為什么有非公平鎖
示例:A,B,C三個(gè)線程,A持有鎖,B阻塞等待獲取鎖
A釋放鎖時(shí),剛好C來(lái)獲取鎖,非公平鎖就可把鎖給C。
(B處于阻塞狀態(tài),喚醒B需要很大開(kāi)銷)
新線程都會(huì)先獲取鎖,獲取不到,再講線程放到隊(duì)列尾,之后按順序喚醒線程獲取鎖。
4.6、讀寫鎖ReadWriteLock
- 讀讀共享,讀寫互斥、寫寫互斥。讀鎖可以被多個(gè)線程獲取,而寫鎖不會(huì)。
- 讀寫鎖適用于讀多寫少的場(chǎng)景。
4.7、讀寫鎖升降級(jí)
4.7.1、非公平鎖下的讀寫操作
- 寫鎖允許插隊(duì):在沒(méi)有其他線程持有讀鎖、寫鎖的時(shí)候,能插隊(duì)成功;插隊(duì)失敗進(jìn)行等待隊(duì)列。
- 讀鎖不允許插隊(duì)(防止饑餓線程)
4.7.2、讀寫鎖的降級(jí)
在不釋放寫鎖的情況下,直接獲取讀鎖,然后再釋放寫鎖。(區(qū)別于直接釋放寫鎖之后再獲取讀鎖---中間有時(shí)間差,其他線程可能獲得鎖)
長(zhǎng)時(shí)間持有寫鎖,浪費(fèi)資源,此時(shí)會(huì)禁止其他線程的讀取。
4.7.3、不支持讀寫鎖的升級(jí):讀鎖->寫鎖。
場(chǎng)景:升級(jí)時(shí),需要等待所有讀鎖都釋放。
示例:A、B兩個(gè)線程持有讀鎖,且都想升級(jí)為寫鎖,且A、B都在等待對(duì)方釋放讀鎖,此時(shí)就會(huì)發(fā)生死鎖現(xiàn)象。
4.7.4、鎖降級(jí)中讀鎖的獲取是否必要呢?
答案是必要的。主要是為了保證數(shù)據(jù)的可見(jiàn)性,如果當(dāng)前線程不獲取讀鎖而是直接釋放寫鎖, 假設(shè)此刻另一個(gè)線程(記作線程T)獲取了寫鎖并修改了數(shù)據(jù),那么當(dāng)前線程無(wú)法感知線程T的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級(jí)的步驟,則線程T將會(huì)被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進(jìn)行數(shù)據(jù)更新。
4.8、自旋鎖
在java1.5及以上的并發(fā)包中,AtomicXXX等原子類基本都是自旋鎖的實(shí)現(xiàn)。
定義
- 自旋鎖:不會(huì)放棄CPU時(shí)間片,不停地嘗試獲取鎖,直到獲取成功為止。
- 非自旋鎖:獲取鎖失敗后,線程休眠、釋放CPU資源。
好處
阻塞和喚醒線程需要高昂開(kāi)銷,自旋鎖可避免上下文切換,節(jié)省線程狀態(tài)切換帶來(lái)的開(kāi)銷。
缺點(diǎn)
如果鎖一直被其他線程持有,那么就會(huì)一直自旋,白白浪費(fèi)處理器資源。
4.9、JVM對(duì)synchronized的優(yōu)化
自旋、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖
4.9.1、自旋
自適應(yīng)的自旋鎖:會(huì)根據(jù)最近自旋嘗試的成功率、失敗率等因素來(lái)決定自旋時(shí)間,以解決自旋鎖長(zhǎng)時(shí)間進(jìn)行無(wú)用自旋的問(wèn)題。
4.9.2、鎖消除
如果某些對(duì)象不可能被其他對(duì)象訪問(wèn)到,就可以把他們當(dāng)成棧上數(shù)據(jù),只能被當(dāng)前線程訪問(wèn),絕對(duì)線程安全。此時(shí),編譯器就會(huì)把synchronized給消除,省去加鎖和解鎖的開(kāi)銷。
4.9.3、鎖粗化
舉例說(shuō)明:
有三個(gè)連續(xù)的同步代碼塊,每執(zhí)行一個(gè)同步代碼塊時(shí),都要進(jìn)行加鎖和解鎖操作。因?yàn)檫B續(xù),此時(shí)可以把三個(gè)同步代碼塊,合并為一個(gè)同步代碼塊,增大了同步區(qū)域,即為鎖粗化。
減少了中間無(wú)意義的加鎖和解鎖過(guò)程。
如果循環(huán)場(chǎng)景使用鎖粗化功能,會(huì)導(dǎo)致其他線程長(zhǎng)時(shí)間無(wú)法獲得鎖,所以鎖粗化功能僅適用于非循環(huán)的場(chǎng)景。
4.9.4、鎖升級(jí)策略
無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖
4.10、鎖升級(jí)策略
- 鎖升級(jí):無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖
- Java對(duì)象頭 = Mark Word + 指向類的指針 + 數(shù)組長(zhǎng)度(只有數(shù)組對(duì)象才有)
- Mark Word記錄對(duì)象和鎖有關(guān)的信息,根據(jù)鎖標(biāo)志位來(lái)確定當(dāng)前對(duì)象是什么鎖狀態(tài)。
[圖片上傳失敗...(image-b132e8-1689324444603)]
4.10.1、無(wú)鎖
鎖標(biāo)志位 = 01,是否偏向鎖 = 0,存儲(chǔ)對(duì)象的hashcode值。
當(dāng)對(duì)象沒(méi)有被當(dāng)成鎖時(shí),Mark Word記錄的是對(duì)象的hashcode。
4.10.2、 無(wú)鎖-> 偏向鎖
鎖標(biāo)志位 = 01,是否偏向鎖 = 1,存儲(chǔ)占有偏向鎖線程的線程id。
當(dāng)對(duì)象被當(dāng)成同步鎖并且線程A通過(guò)CAS搶到鎖時(shí),進(jìn)入偏向鎖狀態(tài),將線程A的線程id寫入Mark Word。
當(dāng)線程A再次試圖獲取鎖時(shí),JVM發(fā)現(xiàn)同步鎖為偏向鎖,且偏向鎖中的線程id值等于線程A的id,就可以直接執(zhí)行同步鎖的代碼塊;
當(dāng)線程B來(lái)試圖獲取鎖時(shí),JVM發(fā)現(xiàn)同步鎖為偏向鎖,且偏向鎖中的線程id值不等于線程B的id,線程B就會(huì)通過(guò)CAS去嘗試獲取鎖:如果獲取成功,則將Mark Word中的偏向鎖的線程id修改為線程B的id;如果獲取失敗,則將同步鎖升級(jí)為輕量級(jí)鎖;
4.10.3、 偏向鎖升級(jí)為輕量級(jí)鎖
出現(xiàn)鎖競(jìng)爭(zhēng)(CAS獲取鎖失敗) 時(shí),偏向鎖升級(jí)為輕量級(jí)鎖。
鎖標(biāo)志位 = 00,存儲(chǔ)指向棧中鎖記錄的指針。
JVM在當(dāng)前線程的棧幀中開(kāi)辟一塊獨(dú)立空間,用以保存指向?qū)ο箧iMark Word的指針,同時(shí)Mark Word保存指向棧幀中獨(dú)立空間的指針。
如果保存成功,則當(dāng)前線程獲取該對(duì)象的輕量級(jí)鎖,執(zhí)行同步代碼;
如果保存失敗,則當(dāng)前線程 自旋,不斷嘗試獲取鎖(嘗試N次,由JVM決定),如果搶鎖成功,執(zhí)行同步代碼;如果搶鎖失敗,則將輕量級(jí)鎖升級(jí)為重量級(jí)鎖。
4.10.4、 輕量級(jí)鎖-> 重量級(jí)鎖
輕量級(jí)鎖自旋超過(guò)JVM指定次數(shù)時(shí),升級(jí)為重量級(jí)鎖。
鎖標(biāo)志位 = 10,存儲(chǔ)指向重量級(jí)鎖的指針。
未搶占到鎖的線程會(huì)被阻塞,搶到鎖的線程在釋放鎖的同時(shí)會(huì)喚醒那些阻塞的線程。
重量級(jí)鎖下,線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài),成本較高。
4.10.5、適用場(chǎng)景
- 偏向鎖:只有一個(gè)線程在臨界區(qū)執(zhí)行,無(wú)需操作系統(tǒng)介入;
- 輕量級(jí)鎖:多個(gè)線程交替進(jìn)入臨界區(qū),競(jìng)爭(zhēng)不激烈,就算有沖突,線程自旋幾次就能獲取鎖;
- 重量級(jí)鎖:多個(gè)線程出現(xiàn)激烈競(jìng)爭(zhēng)。
4.10.6、鎖的優(yōu)缺點(diǎn)比較
[圖片上傳失敗...(image-6a3c1e-1689324444603)]
5、并發(fā)容器
5.1、HashMap為什么線程不安全
- *擴(kuò)容期間取出的值不準(zhǔn)確
擴(kuò)容時(shí)會(huì)創(chuàng)建一個(gè)新的Entry數(shù)組,然后遍歷原Entry數(shù)組,把所有的Entry重新Hash到新數(shù)組。
在這個(gè)過(guò)程中另一個(gè)線程從HashMap取數(shù)據(jù),就有可能取出null。
- *循環(huán)鏈表(死循環(huán))
原因:并發(fā)+頭插
Jdk1.7在高并發(fā)下,HashMap產(chǎn)生死循環(huán),造成CPU100%負(fù)載:HashMap在存儲(chǔ)時(shí),若size超過(guò)當(dāng)前最大容量*負(fù)載因子時(shí),會(huì)增加桶的數(shù)目,進(jìn)行HashMap數(shù)組擴(kuò)容(resize()),在resize過(guò)程中,會(huì)調(diào)用transfer()方法將鏈表轉(zhuǎn)換成新鏈表,在多線程情況下可能導(dǎo)致鏈表回路,從而導(dǎo)致死循環(huán)。
擴(kuò)容時(shí),會(huì)調(diào)用transfer方法將舊的元素重新hash后放到新的table中。
Jdk1.8中HashMap的put元素操作由1.7的頭插改為尾插,避免了死循環(huán)問(wèn)題。
5.2、ConcurrentHashMap
- *JDK1.7
Segment鎖分段技術(shù)
segment數(shù)組(存儲(chǔ)鎖)+hashEntry數(shù)組
采用鎖分段技術(shù)來(lái)保證線程安全,將數(shù)據(jù)分為一段一段的,給每段數(shù)據(jù)各分配一把鎖,不同數(shù)據(jù)段之間的讀寫互不影響,效率高
最大并發(fā)個(gè)數(shù)就是 Segment 的個(gè)數(shù),默認(rèn)是 16
- *JDK1.8
Node數(shù)組+鏈表/紅黑樹(shù)
并發(fā)控制使用 synchronized 和 CAS 來(lái)操作
synchronized只鎖定當(dāng)前鏈表或紅黑二叉樹(shù)的首節(jié)點(diǎn),這樣只要hash不沖突,就不會(huì)產(chǎn)生并發(fā),效率又提升N倍
5.3、為什么Map桶中超過(guò)8個(gè)才轉(zhuǎn)為紅黑樹(shù)
- 鏈表長(zhǎng)度>=8,且容量>=MIN_TREEIFY_CAPACITY(默認(rèn)為64)時(shí),鏈表轉(zhuǎn)為紅黑樹(shù)
- 鏈表長(zhǎng)度
單個(gè)TreeNode需要占用的空間大約是普通Node的兩倍。所以使用紅黑樹(shù),就是用空間換時(shí)間。
泊松分布:
在理想情況下,鏈表長(zhǎng)度符合泊松分布,各個(gè)長(zhǎng)度的命中概率依次遞減,當(dāng)長(zhǎng)度為 8 的時(shí)候,概率僅為 0.00000006。這是一個(gè)小于千萬(wàn)分之一的概率
5.4、ConcurrentHashMap 和 Hashtable 的區(qū)別
- *Hashtable
synchronized -鎖整個(gè)Map
不允許在迭代期間修改內(nèi)容
- *ConcurrentHashMap
CAS + synchronized + Node-鎖一個(gè)Node
允許在迭代期間修改內(nèi)容
5.5、CopyOnWriteArrayList
5.5.1、適用場(chǎng)景
- 對(duì)讀操作性能有要求,對(duì)寫操作性能沒(méi)要求;
- 讀多寫少。
5.5.2、特點(diǎn)
寫入不會(huì)阻塞讀取操作,可以在寫入的同時(shí)讀取。打破了讀寫鎖讀寫互斥的限制
5.5.3、原理
- 在寫入操作時(shí),先復(fù)制一份容器的副本;
- 在副本中進(jìn)行寫操作,并行的讀操作依然在原容器中進(jìn)行;
- 修改完成后,將原容器的引用指向副本容器。
5.5.4、缺陷
- 占用內(nèi)存,在寫操作時(shí),會(huì)同時(shí)存在兩個(gè)對(duì)象占用內(nèi)存;
- 容器元素較多時(shí),復(fù)制開(kāi)銷大;
- 寫入操作會(huì)先寫入副本,對(duì)其他線程來(lái)說(shuō),數(shù)據(jù)不能保證可見(jiàn)性。
6、阻塞隊(duì)列(BlockingQueue )
6.1、阻塞隊(duì)列
特點(diǎn):線程安全。
阻塞功能使得生產(chǎn)者和消費(fèi)者兩端的能力得以平衡,當(dāng)有任何一端速度過(guò)快時(shí),阻塞隊(duì)列便會(huì)把過(guò)快的速度給降下來(lái)。(take()、put())
適用場(chǎng)景:常用于生產(chǎn)者-消費(fèi)者模式
常用實(shí)現(xiàn)類:
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue
6.2、常用方法
拋出異常
add:往隊(duì)列里添加一個(gè)元素,如果隊(duì)列滿了,拋出異常;
remove:刪除元素,如果隊(duì)列為空,拋出異常;
element:返回隊(duì)列的頭部節(jié)點(diǎn),但不刪除,如果隊(duì)列為空,拋出異常。
返回結(jié)果但不拋出異常
offer:插入一個(gè)元素,插入成功返回true,插入失敗(隊(duì)列已滿)返回false;
poll:移除并返回隊(duì)列頭節(jié)點(diǎn),隊(duì)列為空,則返回null;
peek:返回隊(duì)列的頭節(jié)點(diǎn)但不刪除,隊(duì)列為空,則返回null。
阻塞
put:插入一個(gè)元素,如果隊(duì)列滿了,則阻塞;
take:獲取并移除隊(duì)列頭節(jié)點(diǎn),如果隊(duì)列為空,則阻塞。
6.3、常見(jiàn)的阻塞隊(duì)列
- *ArrayBlockingQueue
底層為數(shù)組,有界隊(duì)列,利用ReentrantLock實(shí)現(xiàn)線程安全。可設(shè)置容量和是否公平。
- *LinkedBlockingQueue
底層為鏈表,無(wú)界隊(duì)列。
- *SynchronousQueue
該隊(duì)列容量為0,內(nèi)部不存放任何數(shù)據(jù)嗎,數(shù)據(jù)直接傳遞;
每次放數(shù)據(jù)都會(huì)阻塞,直到有消費(fèi)者來(lái)取數(shù)據(jù);
每次取數(shù)據(jù)都會(huì)阻塞,直到有生產(chǎn)者來(lái)存放數(shù)據(jù)。
- *PriorityBlockingQueue
自定義排序(元素必須支持比較大小),支持優(yōu)先級(jí)的無(wú)界阻塞隊(duì)列。
自定義類實(shí)現(xiàn)compareTo()方法指定元素排序規(guī)則;
初始化算構(gòu)造器參數(shù)comparator指定排序規(guī)則。
- *DelayQueue
具有延遲功能的無(wú)界隊(duì)列;
根據(jù)延遲時(shí)間的長(zhǎng)度,決定元素在隊(duì)列中的位置;
越靠近隊(duì)列頭的代表越早過(guò)期。
6.4、并發(fā)安全原理
- *阻塞隊(duì)列(ArrayBoockingQueue為例)
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
讀操作和寫操作都需要獲得lock獨(dú)占鎖才能進(jìn)行下一步操作;
讀操作時(shí)如果隊(duì)列為空,線程就會(huì)進(jìn)入到讀線程專屬的 notEmpty 的 Condition 的隊(duì)列中去排隊(duì),等待寫線程寫入新的元素;
寫操作時(shí)如果隊(duì)列已滿,此時(shí)寫操作的線程會(huì)進(jìn)入到寫線程專屬的 notFull 隊(duì)列中去排隊(duì),等待讀線程將隊(duì)列元素移除并騰出空間。
- *非阻塞隊(duì)列(ConcurrentLinkedQueue為例)
死循環(huán) + 樂(lè)觀鎖
6.5、如何選擇阻塞隊(duì)列
6.5.1、線程池對(duì)應(yīng)的阻塞隊(duì)列
根據(jù)線程池的容量和隊(duì)列的容量來(lái)選擇
[圖片上傳失敗...(image-304f8d-1689324444603)]
6.5.2、如何選擇
- 功能:排序、延遲等;
- 容量:容量固定、容量無(wú)限、容量為0;
- 能否擴(kuò)容:考慮是否需要?jiǎng)討B(tài)擴(kuò)容;
- 內(nèi)存結(jié)構(gòu):數(shù)組、鏈表;
- 性能:SynchronousQueue(直接傳遞,無(wú)需保存,性能高);LinkedBlockingQueue(兩把鎖)、ArrayBlockingQueue(一把鎖),并發(fā)程度高時(shí),LinkedBlockingQueue性能更好。
7、原子類
7.1、原子類如何利用CAS保證線程安全
7.1.1、原理
循環(huán) + CAS; Unsafe 類是 CAS 的核心類。
CAS操作包括了3個(gè)操作數(shù)(多個(gè)操作,原子性由硬件層面保證):
? 需要讀寫的內(nèi)存位置(V)
? 進(jìn)行比較的預(yù)期值(A)
? 擬寫入的新值(B)
CAS操作邏輯如下:如果內(nèi)存位置V的值等于預(yù)期的A值,則將該位置更新為新值B,否則不進(jìn)行任何操作。許多CAS的操作是自旋的:如果操作不成功,會(huì)一直重試,直到操作成功為止。
Unsafe類中的getAndAddInt方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
7.1.2、原子類
- Atomic 基本類型原子類
AtomicInteger、AtomicLong、AtomicBoolean
以AtomicInteger為例:
public final int get() //獲取當(dāng)前的值
public final int getAndIncrement() //獲取當(dāng)前的值,并自增
public final int getAndDecrement() //獲取當(dāng)前的值,并自減
public final int getAndAdd(int delta) //獲取當(dāng)前的值,并加上預(yù)期的值
boolean compareAndSet(int expect, int update) //如果輸入的數(shù)值等于預(yù)期值,則以原子方式將該值更新為輸入值(update)
- AtomicArray 數(shù)組類型原子
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
數(shù)組里的元素,都可以保證原子性
- AtomicReference 引用類型原子
AtomicReference、AtomicStampedReference、AtomicMarkableReference
讓一個(gè)對(duì)象保證原子性
- AtomicFieldUpdater 升級(jí)類型原子
AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
把一個(gè)普通變量升級(jí)成原子類。
場(chǎng)景:
1、歷史代碼變量,已經(jīng)被聲明為普通變量并廣泛應(yīng)用,修改成本高,可升級(jí)成原子類保證線程安全;
2、每天大部分時(shí)間,該變量不需要保持原子性,只有在少數(shù)一兩次操作時(shí),需要保證原子性。(原子類比普通變量更耗費(fèi)資源)
[圖片上傳失敗...(image-b8a43d-1689324444603)]
- *Adder 累加器
LongAdder、DoubleAdder
- *Accumulator 積累器
LongAccumulator、DoubleAccumulator
7.2、如何解決AtomicInteger在高并發(fā)下的性能問(wèn)題
7.2.1、原因
高并發(fā)下,本地內(nèi)存和共享內(nèi)存間的數(shù)據(jù)同步會(huì)耗費(fèi)大量資源。
[圖片上傳失敗...(image-c0a034-1689324444603)]
7.2.2、解決方案
LongAdder:分段累加
內(nèi)部有兩個(gè)參數(shù)參與計(jì)數(shù):第一個(gè)叫作 base,它是一個(gè)變量,第二個(gè)是 Cell[] ,是一個(gè)數(shù)組。
低并發(fā)時(shí):base變量
可直接將累加結(jié)果修改到base變量上;
高并發(fā)時(shí):Cell數(shù)組
LongAdder 會(huì)通過(guò)計(jì)算出每個(gè)線程的 hash 值來(lái)給線程分配到不同的 Cell 上去,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來(lái)就不會(huì)和其他的計(jì)數(shù)器干擾,Cell 之間并不存在競(jìng)爭(zhēng)關(guān)系,所以在自加的過(guò)程中,就大大減少了剛才的 flush 和 refresh,以及降低了沖突的概率。
最后會(huì)執(zhí)行 LongAdder.sum() ,把各個(gè)線程里的 Cell 累計(jì)求和,并加上 base,形成最終的總和。
7.2.3、適用場(chǎng)景
- LongAdder:場(chǎng)景僅僅是需要用到加和減操作的話。
- AtomicLong:需要利用 CAS 比如 compareAndSet 等復(fù)雜操作。
7.3、原子類和volatile
[圖片上傳失敗...(image-f36729-1689324444603)]
- volatile:保證變量可見(jiàn)性,變量每次修改會(huì)直接寫入共享內(nèi)存。
- 原子類:保證i++等復(fù)合操作的原子性。
- synchronized:既能保證原子性,也能保證可見(jiàn)性。
7.4、AtomicInteger 和 synchronized 的異同點(diǎn)?
- *AtomicInteger:
CAS保證線程安全;
只能修飾一個(gè)對(duì)象;
性能:樂(lè)觀鎖,開(kāi)銷隨著時(shí)間增加,逐步上漲。
- *synchronized:
monitor 鎖保證線程安全;
可以鎖方法或代碼塊;
性能:悲觀鎖,開(kāi)銷固定。JDK6之后的鎖升級(jí)會(huì)提升性能。
7.5、Java 8 中 Adder 和 Accumulator 有什么區(qū)別?
- *Adder:
分段鎖(降低沖突)。base(低并發(fā)),Cell[]數(shù)組(高并發(fā))。
在高并發(fā)下 LongAdder 比 AtomicLong 效率更高
- *Accumulator:
Accumulator 就是一個(gè)更通用版本的 Adder。
LongAdder 的 API 只有對(duì)數(shù)值的加減,而 LongAccumulator 提供了自定義的函數(shù)操作。
8、ThreadLocal
8.1、適用場(chǎng)景
用作保存每個(gè)線程獨(dú)享的對(duì)象,為每個(gè)線程都創(chuàng)建一個(gè)副本。
每個(gè)線程需要獨(dú)立保存信息,以便當(dāng)前線程內(nèi)其他方法使用。類似于線程內(nèi)的全局變量。
省去一步步傳參的步驟。
示例:simpleDateFormat對(duì)象;
1000個(gè)線程,共享一個(gè)simpleDateFormat對(duì)象------不安全。
1000個(gè)線程,每個(gè)線程創(chuàng)建一個(gè)simpleDateFormat 對(duì)象------占用內(nèi)存大,性能低;
線程池,容量為16,設(shè)置simpleDateFormat為每個(gè)線程的ThreadLocal變量,一共就16個(gè)simpleDateFormat對(duì)象。
8.2、ThreadLocal是用來(lái)解決共享資源的多線程訪問(wèn)問(wèn)題嗎?
不是。ThreadLocal存儲(chǔ)的對(duì)象是線程獨(dú)享的,不是共享資源。
注意:當(dāng)放入ThreadLocal中的對(duì)象是static修飾的,那么ThreadLocal并不能解決線程安全問(wèn)題。
ThreadLocal 是通過(guò)讓每個(gè)線程獨(dú)享自己的副本,避免了資源的競(jìng)爭(zhēng)。
synchronized 主要用于臨界資源的分配,在同一時(shí)刻限制最多只有一個(gè)線程能訪問(wèn)該資源。
8.3、多個(gè)ThreadLocal在Thread的threadlocals里是如何存儲(chǔ)的?
Thread>ThreadLocalMap>ThreadLocal
[圖片上傳失敗...(image-fbb76b-1689324444603)]
一個(gè) Thread 里面只有一個(gè)ThreadLocalMap ,而在一個(gè) ThreadLocalMap 里面卻可以有很多的 ThreadLocal,每一個(gè) ThreadLocal 都對(duì)應(yīng)一個(gè) value。
如果在一個(gè)想在Thread中保存多個(gè)ThreadLocal對(duì)象,那么直接new多個(gè)ThreadLocal存儲(chǔ)對(duì)象即可。
ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set("string1");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("string2");
8.4、內(nèi)存泄露
8.4.1、key的內(nèi)存泄露:
ThreadLocalMap里的Entry中的key是弱引用。
如果在Entry中強(qiáng)引用了ThreadLocal,盡管執(zhí)行 ThreadLocal instance = null,
根據(jù)可達(dá)性分析算法,該ThreadLocal對(duì)象仍然是可達(dá)的,就不會(huì)被回收。
8.4.2、value的內(nèi)存泄露:
正常情況下,當(dāng)線程終止,key 所對(duì)應(yīng)的 value 是可以被正常垃圾回收的,因?yàn)闆](méi)有任何強(qiáng)引用存在了。但是有時(shí)線程的生命周期是很長(zhǎng)的,如果線程遲遲不會(huì)終止,那么可能 ThreadLocal 以及它所對(duì)應(yīng)的 value 早就不再有用了。在這種情況下,我們應(yīng)該保證它們都能夠被正常的回收。
8.4.3、避免內(nèi)存泄露:
調(diào)用 ThreadLocal 的 remove 方法。調(diào)用這個(gè)方法就可以刪除對(duì)應(yīng)的 value 對(duì)象,可以避免內(nèi)存泄漏。
以上內(nèi)容為個(gè)人學(xué)習(xí)理解,如有問(wèn)題,歡迎在評(píng)論區(qū)指出。
部分內(nèi)容截取自網(wǎng)絡(luò),如有侵權(quán),聯(lián)系作者刪除。