Java改知能机_Java 面试突击之 Java 并发知识基础 进阶考点全解析
版權(quán)說明:本文內(nèi)容根據(jù) github 開源項(xiàng)目整理所得
項(xiàng)目地址:https://github.com/Snailclimb/JavaGuide?github.com
一、基礎(chǔ)
什么是線程和進(jìn)程?
何為進(jìn)程?
進(jìn)程是程序的一次執(zhí)行過程,是系統(tǒng)運(yùn)行程序的基本單位,因此進(jìn)程是動(dòng)態(tài)的。系統(tǒng)運(yùn)行一個(gè)程序即是一個(gè)進(jìn)程從創(chuàng)建,運(yùn)行到消亡的過程。
在 Java 中,當(dāng)我們啟動(dòng) main 函數(shù)時(shí)其實(shí)就是啟動(dòng)了一個(gè) JVM 的進(jìn)程,而 main 函數(shù)所在的線程就是這個(gè)進(jìn)程中的一個(gè)線程,也稱主線程。
如下圖所示,在 windows 中通過查看任務(wù)管理器的方式,我們就可以清楚看到 window 當(dāng)前運(yùn)行的進(jìn)程(.exe 文件的運(yùn)行)。
何為線程?
線程與進(jìn)程相似,但線程是一個(gè)比進(jìn)程更小的執(zhí)行單位。一個(gè)進(jìn)程在其執(zhí)行的過程中可以產(chǎn)生多個(gè)線程。與進(jìn)程不同的是同類的多個(gè)線程共享進(jìn)程的堆和方法區(qū)資源,但每個(gè)線程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧,所以系統(tǒng)在產(chǎn)生一個(gè)線程,或是在各個(gè)線程之間作切換工作時(shí),負(fù)擔(dān)要比進(jìn)程小得多,也正因?yàn)槿绱?#xff0c;線程也被稱為輕量級進(jìn)程。
Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個(gè)普通的 Java 程序有哪些線程,代碼如下。
public class MultiThread {
public static void main(String[] args) {
// 獲取 Java 線程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍歷線程信息,僅打印線程 ID 和線程名稱信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分發(fā)處理給 JVM 信號的線程
[3] Finalizer //調(diào)用對象 finalize 方法的線程
[2] Reference Handler //清除 reference 線程
[1] main //main 線程,程序入口
如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時(shí)都想申請對方的資源,所以這兩個(gè)線程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)。
多個(gè)線程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。
認(rèn)識線程死鎖
什么是線程死鎖?如何避免死鎖?
Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少。
上下文切換通常是計(jì)算密集型的。也就是說,它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時(shí)間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時(shí)間,事實(shí)上,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。
概括來說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換會(huì)這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換。
多線程編程中一般線程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線程使用,這個(gè)過程就屬于一次上下文切換。
什么是上下文切換?
當(dāng)線程執(zhí)行 wait()方法之后,線程進(jìn)入 **WAITING(等待)**狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而 TIME_WAITING(超時(shí)等待) 狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態(tài)。當(dāng)超時(shí)時(shí)間到達(dá)后 Java 線程將會(huì)返回到 RUNNABLE 狀態(tài)。當(dāng)線程調(diào)用同步方法時(shí),在沒有獲取到鎖的情況下,線程將會(huì)進(jìn)入到 BLOCKED(阻塞) 狀態(tài)。線程在執(zhí)行 Runnable 的run()方法之后將會(huì)進(jìn)入到 TERMINATED(終止) 狀態(tài)。
RUNNABLE-VS-RUNNING
操作系統(tǒng)隱藏 Java 虛擬機(jī)(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài),它只能看到 RUNNABLE 狀態(tài)(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系統(tǒng)一般將這兩個(gè)狀態(tài)統(tǒng)稱為 RUNNABLE(運(yùn)行中) 狀態(tài) 。
由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start() 方法后開始運(yùn)行,線程這時(shí)候處于 READY(可運(yùn)行) 狀態(tài)。可運(yùn)行狀態(tài)的線程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。
Java 線程狀態(tài)變遷
線程在生命周期中并不是固定處于某一個(gè)狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。Java 線程狀態(tài)變遷如下圖所示(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié)):
Java 線程的狀態(tài)
Java 線程在運(yùn)行的生命周期中的指定時(shí)刻只可能處于下面 6 種不同狀態(tài)的其中一個(gè)狀態(tài)(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié))。
說說線程的生命周期和狀態(tài)?
并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度,但是并發(fā)編程并不總是能提高程序運(yùn)行速度的,而且并發(fā)編程可能會(huì)遇到很多問題,比如:內(nèi)存泄漏、上下文切換、死鎖還有受限于硬件和軟件的資源閑置問題。
使用多線程可能帶來什么問題?單核時(shí)代: 在單核時(shí)代多線程主要是為了提高 CPU 和 IO 設(shè)備的綜合利用率。舉個(gè)例子:當(dāng)只有一個(gè)線程的時(shí)候會(huì)導(dǎo)致 CPU 計(jì)算時(shí),IO 設(shè)備空閑;進(jìn)行 IO 操作時(shí),CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當(dāng)有兩個(gè)線程的時(shí)候就不一樣了,當(dāng)一個(gè)線程執(zhí)行 CPU 計(jì)算時(shí),另外一個(gè)線程可以進(jìn)行 IO 操作,這樣兩個(gè)的利用率就可以在理想情況下達(dá)到 100%了。
多核時(shí)代: 多核時(shí)代多線程主要是為了提高 CPU 利用率。舉個(gè)例子:假如我們要計(jì)算一個(gè)復(fù)雜的任務(wù),我們只用一個(gè)線程的話,CPU 只會(huì)一個(gè) CPU 核心被利用到,而創(chuàng)建多個(gè)線程就可以讓多個(gè) CPU 核心被利用到,這樣就提高了 CPU 的利用率。
再深入到計(jì)算機(jī)底層來探討:**從計(jì)算機(jī)底層來說:**線程可以比作是輕量級的進(jìn)程,是程序執(zhí)行的最小單位,線程間的切換和調(diào)度的成本遠(yuǎn)遠(yuǎn)小于進(jìn)程。另外,多核 CPU 時(shí)代意味著多個(gè)線程可以同時(shí)運(yùn)行,這減少了線程上下文切換的開銷。
**從當(dāng)代互聯(lián)網(wǎng)發(fā)展趨勢來說:**現(xiàn)在的系統(tǒng)動(dòng)不動(dòng)就要求百萬級甚至千萬級的并發(fā)量,而多線程并發(fā)編程正是開發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線程機(jī)制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。
先從總體上來說:
為什么要使用多線程呢?并發(fā): 同一時(shí)間段,多個(gè)任務(wù)都在執(zhí)行 (單位時(shí)間內(nèi)不一定同時(shí)執(zhí)行);
**并行:**單位時(shí)間內(nèi),多個(gè)任務(wù)同時(shí)執(zhí)行。
說說并發(fā)與并行的區(qū)別?
堆和方法區(qū)是所有線程共享的資源,其中堆是進(jìn)程中最大的一塊內(nèi)存,主要用于存放新創(chuàng)建的對象 (所有對象都在這里分配內(nèi)存),方法區(qū)主要用于存放已被加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
一句話簡單了解堆和方法區(qū)
所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機(jī)棧和本地方法棧是線程私有的。**虛擬機(jī)棧:**每個(gè) Java 方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀用于存儲局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個(gè)棧幀在 Java 虛擬機(jī)棧中入棧和出棧的過程。
**本地方法棧:**和虛擬機(jī)棧所發(fā)揮的作用非常相似,區(qū)別是: 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。 在 HotSpot 虛擬機(jī)中和 Java 虛擬機(jī)棧合二為一。
虛擬機(jī)棧和本地方法棧為什么是私有的?
所以,程序計(jì)數(shù)器私有主要是為了線程切換后能恢復(fù)到正確的執(zhí)行位置。
需要注意的是,如果執(zhí)行的是 native 方法,那么程序計(jì)數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時(shí)程序計(jì)數(shù)器記錄的才是下一條指令的地址。字節(jié)碼解釋器通過改變程序計(jì)數(shù)器來依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
在多線程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切換回來的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了。
程序計(jì)數(shù)器主要有下面兩個(gè)作用:
程序計(jì)數(shù)器為什么是私有的?
下面來思考這樣一個(gè)問題:為什么程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧是線程私有的呢?為什么堆和方法區(qū)是線程共享的呢?
下面是該知識點(diǎn)的擴(kuò)展內(nèi)容!
總結(jié): 線程 是 進(jìn)程 劃分成的更小的運(yùn)行單位。線程和進(jìn)程最大的不同在于基本上各進(jìn)程是獨(dú)立的,而各線程則不一定,因?yàn)橥贿M(jìn)程中的線程極有可能會(huì)相互影響。線程執(zhí)行開銷小,但不利于資源的管理和保護(hù);而進(jìn)程正相反
從上圖可以看出:一個(gè)進(jìn)程中可以有多個(gè)線程,多個(gè)線程共享進(jìn)程的堆和方法區(qū) (JDK1.8 之后的元空間)資源,但是每個(gè)線程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧 和 本地方法棧。
下圖是 Java 內(nèi)存區(qū)域,通過下圖我們從 JVM 的角度來說一下線程和進(jìn)程之間的關(guān)系。如果你對 Java 內(nèi)存區(qū)域 (運(yùn)行時(shí)數(shù)據(jù)區(qū)) 這部分知識不太了解的話可以閱讀一下這篇文章:《可能是把 Java 內(nèi)存區(qū)域講的最清楚的一篇文章》
圖解進(jìn)程和線程的關(guān)系
從 JVM 角度說進(jìn)程和線程之間的關(guān)系
請簡要描述線程與進(jìn)程的關(guān)系,區(qū)別及優(yōu)缺點(diǎn)?
從上面的輸出內(nèi)容可以看出:一個(gè) Java 程序的運(yùn)行是 main 線程和多個(gè)其他線程同時(shí)運(yùn)行。
上述程序輸出如下(輸出內(nèi)容可能不同,不用太糾結(jié)下面每個(gè)線程的作用,只用知道 main 線程執(zhí)行 main 方法即可):
二、進(jìn)階考點(diǎn)
1. synchronized 關(guān)鍵字
1.1. 說一說自己對于 synchronized 關(guān)鍵字的了解
synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線程執(zhí)行。
另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個(gè)線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間,時(shí)間成本相對較高,這也是為什么早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯(cuò)了。JDK1.6對鎖的實(shí)現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。
1.2. 說說自己是怎么使用 synchronized 關(guān)鍵字,在項(xiàng)目中用到了嗎
synchronized關(guān)鍵字最主要的三種使用方式:修飾實(shí)例方法: 作用于當(dāng)前對象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對象實(shí)例的鎖
修飾靜態(tài)方法: :也就是給當(dāng)前類加鎖,會(huì)作用于類的所有對象實(shí)例,因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對象,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源,不管new了多少個(gè)對象,只有一份)。所以如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個(gè)實(shí)例對象所屬類的靜態(tài) synchronized 方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對象鎖。
修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖。
總結(jié): synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關(guān)鍵字加到實(shí)例方法上是給對象實(shí)例上鎖。盡量不要使用 synchronized(String a) 因?yàn)镴VM中,字符串常量池具有緩存功能!
下面我以一個(gè)常見的面試題為例講解一下 synchronized 關(guān)鍵字的具體使用。
面試中面試官經(jīng)常會(huì)說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗(yàn)鎖方式實(shí)現(xiàn)單例模式的原理唄!”
雙重校驗(yàn)鎖實(shí)現(xiàn)對象單例(線程安全)
public class Singleton {
?
private volatile static Singleton uniqueInstance;
?
private Singleton() {
}
?
public static Singleton getUniqueInstance() {
//先判斷對象是否已經(jīng)實(shí)例過,沒有實(shí)例化過才進(jìn)入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}volatile關(guān)鍵字是線程同步的輕量級實(shí)現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊。synchronized關(guān)鍵字在JavaSE1.6之后進(jìn)行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實(shí)際開發(fā)中使用 synchronized 關(guān)鍵字的場景還是更多一些。
多線程訪問volatile關(guān)鍵字不會(huì)發(fā)生阻塞,而synchronized關(guān)鍵字可能會(huì)發(fā)生阻塞
volatile關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。synchronized關(guān)鍵字兩者都能保證。
volatile關(guān)鍵字主要用于解決變量在多個(gè)線程之間的可見性,而 synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性。
3. ThreadLocal
3.1. ThreadLocal簡介
通常情況下,我們創(chuàng)建的變量是可以被任何一個(gè)線程訪問并修改的。如果想實(shí)現(xiàn)每一個(gè)線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個(gè)線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲每個(gè)線程的私有數(shù)據(jù)。
如果你創(chuàng)建了一個(gè)ThreadLocal變量,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認(rèn)值或?qū)⑵渲蹈臑楫?dāng)前線程所存的副本的值,從而避免了線程安全問題。
再舉個(gè)簡單的例子:
比如有兩個(gè)人去寶屋收集寶物,這兩個(gè)共用一個(gè)袋子的話肯定會(huì)產(chǎn)生爭執(zhí),但是給他們兩個(gè)人每個(gè)人分配一個(gè)袋子的話就不會(huì)出現(xiàn)這樣的問題。如果把這兩個(gè)人比作線程的話,那么ThreadLocal就是用來這兩個(gè)線程競爭的。
3.2. ThreadLocal示例
相信看了上面的解釋,大家已經(jīng)搞懂 ThreadLocal 類是個(gè)什么東西了。
import java.text.SimpleDateFormat;
import java.util.Random;
?
public class ThreadLocalExample implements Runnable{
?
// SimpleDateFormat 不是線程安全的,所以每個(gè)線程都要有自己獨(dú)立的副本
private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
?
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for(int i=0 ; i<10; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
?
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
formatter.set(new SimpleDateFormat());
?
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
?
}
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm
private static final ThreadLocal formatter = new ThreadLocal(){
@Override
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public class Thread implements Runnable {
......
//與此線程有關(guān)的ThreadLocal值。由ThreadLocal類維護(hù)
ThreadLocal.ThreadLocalMap threadLocals = null;
?
//與此線程有關(guān)的InheritableThreadLocal值。由InheritableThreadLocal類維護(hù)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。
每個(gè)Thread中都具備一個(gè)ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal為key的鍵值對。這里解釋了為什么每個(gè)線程訪問同一個(gè)ThreadLocal,得到的確是不同的數(shù)值。另外,ThreadLocal 是 map結(jié)構(gòu)是為了讓每個(gè)線程可以關(guān)聯(lián)多個(gè) ThreadLocal變量。
通過上面這些內(nèi)容,我們足以通過猜測得出結(jié)論:最終的變量是放在了當(dāng)前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。
ThreadLocal類的set()方法
從上面Thread類 源代碼可以看出Thread 類中有一個(gè) threadLocals 和 一個(gè)inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實(shí)現(xiàn)的定制化的 HashMap。默認(rèn)情況下這兩個(gè)變量都是null,只有當(dāng)前線程調(diào)用 ThreadLocal 類的 set或get方法時(shí)才創(chuàng)建它們,實(shí)際上調(diào)用這兩個(gè)方法的時(shí)候,我們調(diào)用的是ThreadLocalMap類對應(yīng)的 get()、set()方法。
從 Thread類源代碼入手。
3.3. ThreadLocal原理
上面有一段代碼用到了創(chuàng)建 ThreadLocal 變量的那段代碼用到了 Java8 的知識,它等于下面這段代碼,如果你寫了下面這段代碼的話,IDEA會(huì)提示你轉(zhuǎn)換為Java8的格式(IDEA真的不錯(cuò)!)。因?yàn)門hreadLocal類在Java 8中擴(kuò)展,使用一個(gè)新的方法withInitial(),將Supplier功能接口作為參數(shù)。
從輸出中可以看出,Thread-0已經(jīng)改變了formatter的值,但仍然是thread-2默認(rèn)格式化程序與初始化值相同,其他線程也一樣。
Output:
synchronized關(guān)鍵字和volatile關(guān)鍵字比較
2.2. 說說 synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
說白了, volatile 關(guān)鍵字的主要作用就是保證變量的可見性然后還有一個(gè)作用是防止指令重排序。
要解決這個(gè)問題,就需要把變量聲明為volatile,這就指示 JVM,這個(gè)變量是不穩(wěn)定的,每次使用它都到主存中進(jìn)行讀取。
在 JDK1.2 之前,Java的內(nèi)存模型實(shí)現(xiàn)總是從主存(即共享內(nèi)存)讀取變量,是不需要進(jìn)行特別的注意的。而在當(dāng)前的 Java 內(nèi)存模型下,線程可以把變量保存本地內(nèi)存比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫。這就可能造成一個(gè)線程在主存中修改了一個(gè)變量的值,而另外一個(gè)線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。
2.1. 講一下Java內(nèi)存模型
2. volatile關(guān)鍵字
④ 性能已不是選擇標(biāo)準(zhǔn)
如果你想使用上述功能,那么選擇ReentrantLock是一個(gè)不錯(cuò)的選擇。ReentrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過lock.lockInterruptibly()來實(shí)現(xiàn)這個(gè)機(jī)制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認(rèn)情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。
synchronized關(guān)鍵字與wait()和notify()/notifyAll()方法相結(jié)合可以實(shí)現(xiàn)等待/通知機(jī)制,ReentrantLock類當(dāng)然也可以實(shí)現(xiàn),但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的靈活性,比如可以實(shí)現(xiàn)多路通知功能也就是在一個(gè)Lock對象中可以創(chuàng)建多個(gè)Condition實(shí)例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition中,從而可以有選擇性的進(jìn)行線程通知,在調(diào)度線程上更加靈活。 在使用notify()/notifyAll()方法進(jìn)行通知時(shí),被通知的線程是由 JVM 選擇的,用ReentrantLock類結(jié)合Condition實(shí)例可以實(shí)現(xiàn)“選擇性通知” ,這個(gè)功能非常重要,而且是Condition接口默認(rèn)提供的。而synchronized關(guān)鍵字就相當(dāng)于整個(gè)Lock對象中只有一個(gè)Condition實(shí)例,所有的線程都注冊在它一個(gè)身上。如果執(zhí)行notifyAll()方法的話就會(huì)通知所有處于等待狀態(tài)的線程這樣會(huì)造成很大的效率問題,而Condition實(shí)例的signalAll()方法 只會(huì)喚醒注冊在該Condition實(shí)例中的所有等待線程。
相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點(diǎn):①等待可中斷;②可實(shí)現(xiàn)公平鎖;③可實(shí)現(xiàn)選擇性通知(鎖可以綁定多個(gè)條件)
③ ReentrantLock 比 synchronized 增加了一些高級功能
synchronized 是依賴于 JVM 實(shí)現(xiàn)的,前面我們也講到了 虛擬機(jī)團(tuán)隊(duì)在 JDK1.6 為 synchronized 關(guān)鍵字進(jìn)行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機(jī)層面實(shí)現(xiàn)的,并沒有直接暴露給我們。ReentrantLock 是 JDK 層面實(shí)現(xiàn)的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實(shí)現(xiàn)的。
② synchronized 依賴于 JVM 而 ReentrantLock 依賴于 API
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖。比如一個(gè)線程獲得了某個(gè)對象的鎖,此時(shí)這個(gè)對象鎖還沒有釋放,當(dāng)其再次想要獲取這個(gè)對象的鎖的時(shí)候還是可以獲取的,如果不可鎖重入的話,就會(huì)造成死鎖。同一個(gè)線程每次獲取鎖,鎖的計(jì)數(shù)器都自增1,所以要等到鎖的計(jì)數(shù)器下降為0時(shí)才能釋放鎖。
① 兩者都是可重入鎖
1.5. 談?wù)?synchronized和ReentrantLock 的區(qū)別
鎖主要存在四種狀態(tài),依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),他們會(huì)隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
JDK1.6 對鎖的實(shí)現(xiàn)引入了大量的優(yōu)化,如偏向鎖、輕量級鎖、自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化等技術(shù)來減少鎖操作的開銷。
1.4. 說說 JDK1.6 之后的synchronized 關(guān)鍵字底層做了哪些優(yōu)化,可以詳細(xì)介紹一下這些優(yōu)化嗎
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識,該標(biāo)識指明了該方法是一個(gè)同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
synchronized關(guān)鍵字原理
② synchronized 修飾方法的的情況
synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。 當(dāng)執(zhí)行 monitorenter 指令時(shí),線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個(gè)Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權(quán)。當(dāng)計(jì)數(shù)器為0則可以成功獲取,獲取后將鎖計(jì)數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止。
從上面我們可以看出:
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class。
① synchronized 同步語句塊的情況
synchronized 關(guān)鍵字底層原理屬于 JVM 層面。
1.3. 講一下 synchronized 關(guān)鍵字的底層原理
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會(huì)出先問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例。例如,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時(shí) uniqueInstance 還未被初始化。為 uniqueInstance 分配內(nèi)存空間
初始化 uniqueInstance
將 uniqueInstance 指向分配的內(nèi)存地址
uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:
另外,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要。
總結(jié)
以上是生活随笔為你收集整理的Java改知能机_Java 面试突击之 Java 并发知识基础 进阶考点全解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天方夜谭的作者是谁啊?
- 下一篇: 30分钻戒多少钱啊?