日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析

發(fā)布時間:2025/3/11 java 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

版權(quán)說明:本文內(nèi)容根據(jù) github 開源項目整理所得

項目地址:

https://github.com/Snailclimb/JavaGuide?github.com

一、基礎(chǔ)

什么是線程和進程?

何為進程?

進程是程序的一次執(zhí)行過程,是系統(tǒng)運行程序的基本單位,因此進程是動態(tài)的。系統(tǒng)運行一個程序即是一個進程從創(chuàng)建,運行到消亡的過程。

在 Java 中,當我們啟動 main 函數(shù)時其實就是啟動了一個 JVM 的進程,而 main 函數(shù)所在的線程就是這個進程中的一個線程,也稱主線程。

如下圖所示,在 windows 中通過查看任務(wù)管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。

何為線程?

線程與進程相似,但線程是一個比進程更小的執(zhí)行單位。一個進程在其執(zhí)行的過程中可以產(chǎn)生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區(qū)資源,但每個線程有自己的程序計數(shù)器、虛擬機棧和本地方法棧,所以系統(tǒng)在產(chǎn)生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。

Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下。

public class MultiThread {public static void main(String[] args) {// 獲取 Java 線程管理 MXBeanThreadMXBean 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,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態(tài)。

多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。

認識線程死鎖

什么是線程死鎖?如何避免死鎖?

Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統(tǒng)中時間消耗最大的操作。

概括來說就是:當前任務(wù)在執(zhí)行完 CPU 時間片切換到另一個任務(wù)之前會先保存自己的狀態(tài),以便下次再切換會這個任務(wù)時,可以再加載這個任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換。

多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。

什么是上下文切換?

當線程執(zhí)行 wait()方法之后,線程進入 **WAITING(等待)**狀態(tài)。進入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運行狀態(tài),而 TIME_WAITING(超時等待) 狀態(tài)相當于在等待狀態(tài)的基礎(chǔ)上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態(tài)。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態(tài)。當線程調(diào)用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態(tài)。線程在執(zhí)行 Runnable 的run()方法之后將會進入到 TERMINATED(終止) 狀態(tài)。

RUNNABLE-VS-RUNNING

操作系統(tǒng)隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài),它只能看到 RUNNABLE 狀態(tài)(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系統(tǒng)一般將這兩個狀態(tài)統(tǒng)稱為 RUNNABLE(運行中) 狀態(tài) 。

由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start() 方法后開始運行,線程這時候處于 READY(可運行) 狀態(tài)。可運行狀態(tài)的線程獲得了 CPU 時間片(timeslice)后就處于 RUNNING(運行) 狀態(tài)。

Java 線程狀態(tài)變遷

線程在生命周期中并不是固定處于某一個狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。Java 線程狀態(tài)變遷如下圖所示(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié)):

Java 線程的狀態(tài)

Java 線程在運行的生命周期中的指定時刻只可能處于下面 6 種不同狀態(tài)的其中一個狀態(tài)(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié))。

說說線程的生命周期和狀態(tài)?

并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運行速度,但是并發(fā)編程并不總是能提高程序運行速度的,而且并發(fā)編程可能會遇到很多問題,比如:內(nèi)存泄漏、上下文切換、死鎖還有受限于硬件和軟件的資源閑置問題。

使用多線程可能帶來什么問題?

  • 單核時代: 在單核時代多線程主要是為了提高 CPU 和 IO 設(shè)備的綜合利用率。舉個例子:當只有一個線程的時候會導(dǎo)致 CPU 計算時,IO 設(shè)備空閑;進行 IO 操作時,CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執(zhí)行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
  • 多核時代: 多核時代多線程主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個復(fù)雜的任務(wù),我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創(chuàng)建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

再深入到計算機底層來探討:

  • **從計算機底層來說:**線程可以比作是輕量級的進程,是程序執(zhí)行的最小單位,線程間的切換和調(diào)度的成本遠遠小于進程。另外,多核 CPU 時代意味著多個線程可以同時運行,這減少了線程上下文切換的開銷。
  • **從當代互聯(lián)網(wǎng)發(fā)展趨勢來說:**現(xiàn)在的系統(tǒng)動不動就要求百萬級甚至千萬級的并發(fā)量,而多線程并發(fā)編程正是開發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線程機制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。

先從總體上來說:

為什么要使用多線程呢?

  • 并發(fā): 同一時間段,多個任務(wù)都在執(zhí)行 (單位時間內(nèi)不一定同時執(zhí)行);
  • **并行:**單位時間內(nèi),多個任務(wù)同時執(zhí)行。

說說并發(fā)與并行的區(qū)別?

堆和方法區(qū)是所有線程共享的資源,其中堆是進程中最大的一塊內(nèi)存,主要用于存放新創(chuàng)建的對象 (所有對象都在這里分配內(nèi)存),方法區(qū)主要用于存放已被加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。

一句話簡單了解堆和方法區(qū)

所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

  • **虛擬機棧:**每個 Java 方法在執(zhí)行的同時會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
  • **本地方法棧:**和虛擬機棧所發(fā)揮的作用非常相似,區(qū)別是: 虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。

虛擬機棧和本地方法棧為什么是私有的?

所以,程序計數(shù)器私有主要是為了線程切換后能恢復(fù)到正確的執(zhí)行位置。

需要注意的是,如果執(zhí)行的是 native 方法,那么程序計數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時程序計數(shù)器記錄的才是下一條指令的地址。

  • 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
  • 在多線程的情況下,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。
  • 程序計數(shù)器主要有下面兩個作用:

    程序計數(shù)器為什么是私有的?

    下面來思考這樣一個問題:為什么程序計數(shù)器、虛擬機棧和本地方法棧是線程私有的呢?為什么堆和方法區(qū)是線程共享的呢?

    下面是該知識點的擴展內(nèi)容!

    總結(jié): 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在于基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執(zhí)行開銷小,但不利于資源的管理和保護;而進程正相反

    從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的堆和方法區(qū) (JDK1.8 之后的元空間)資源,但是每個線程有自己的程序計數(shù)器、虛擬機棧 和 本地方法棧。

    下圖是 Java 內(nèi)存區(qū)域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關(guān)系。如果你對 Java 內(nèi)存區(qū)域 (運行時數(shù)據(jù)區(qū)) 這部分知識不太了解的話可以閱讀一下這篇文章:《可能是把 Java 內(nèi)存區(qū)域講的最清楚的一篇文章》

    圖解進程和線程的關(guān)系

    從 JVM 角度說進程和線程之間的關(guān)系

    請簡要描述線程與進程的關(guān)系,區(qū)別及優(yōu)缺點?

    從上面的輸出內(nèi)容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行。

    上述程序輸出如下(輸出內(nèi)容可能不同,不用太糾結(jié)下面每個線程的作用,只用知道 main 線程執(zhí)行 main 方法即可):

    二、進階考點

    1. synchronized 關(guān)鍵字

    1.1. 說一說自己對于 synchronized 關(guān)鍵字的了解

    synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。

    另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯了。JDK1.6對鎖的實現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。

    1.2. 說說自己是怎么使用 synchronized 關(guān)鍵字,在項目中用到了嗎

    synchronized關(guān)鍵字最主要的三種使用方式:

    • 修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
    • 修飾靜態(tài)方法: :也就是給當前類加鎖,會作用于類的所有對象實例,因為靜態(tài)成員不屬于任何一個實例對象,是類成員( static 表明這是該類的一個靜態(tài)資源,不管new了多少個對象,只有一份)。所以如果一個線程A調(diào)用一個實例對象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized 方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當前實例對象鎖。
    • 修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

    總結(jié): synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關(guān)鍵字加到實例方法上是給對象實例上鎖。盡量不要使用 synchronized(String a) 因為JVM中,字符串常量池具有緩存功能!

    下面我以一個常見的面試題為例講解一下 synchronized 關(guān)鍵字的具體使用。

    面試中面試官經(jīng)常會說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現(xiàn)單例模式的原理唄!”

    雙重校驗鎖實現(xiàn)對象單例(線程安全)

    public class Singleton {?private volatile static Singleton uniqueInstance;?private Singleton() {}?public static Singleton getUniqueInstance() {//先判斷對象是否已經(jīng)實例過,沒有實例化過才進入加鎖代碼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)鍵字是線程同步的輕量級實現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊。synchronized關(guān)鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實際開發(fā)中使用 synchronized 關(guān)鍵字的場景還是更多一些。
    • 多線程訪問volatile關(guān)鍵字不會發(fā)生阻塞,而synchronized關(guān)鍵字可能會發(fā)生阻塞
    • volatile關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。synchronized關(guān)鍵字兩者都能保證。
    • volatile關(guān)鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性。

    3. ThreadLocal

    3.1. ThreadLocal簡介

    通常情況下,我們創(chuàng)建的變量是可以被任何一個線程訪問并修改的。如果想實現(xiàn)每一個線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲每個線程的私有數(shù)據(jù)。

    如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認值或?qū)⑵渲蹈臑楫斍熬€程所存的副本的值,從而避免了線程安全問題。

    再舉個簡單的例子:

    比如有兩個人去寶屋收集寶物,這兩個共用一個袋子的話肯定會產(chǎn)生爭執(zhí),但是給他們兩個人每個人分配一個袋子的話就不會出現(xiàn)這樣的問題。如果把這兩個人比作線程的話,那么ThreadLocal就是用來這兩個線程競爭的。

    3.2. ThreadLocal示例

    相信看了上面的解釋,大家已經(jīng)搞懂 ThreadLocal 類是個什么東西了。

    import java.text.SimpleDateFormat;import java.util.Random;?public class ThreadLocalExample implements Runnable{?// SimpleDateFormat 不是線程安全的,所以每個線程都要有自己獨立的副本private static final ThreadLocal<SimpleDateFormat> 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();}}?@Overridepublic 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 threadsformatter.set(new SimpleDateFormat());?System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());}?}Thread Name= 0 default Formatter = yyyyMMdd HHmmThread Name= 0 formatter = yy-M-d ah:mmThread Name= 1 default Formatter = yyyyMMdd HHmmThread Name= 2 default Formatter = yyyyMMdd HHmmThread Name= 1 formatter = yy-M-d ah:mmThread Name= 3 default Formatter = yyyyMMdd HHmmThread Name= 2 formatter = yy-M-d ah:mmThread Name= 4 default Formatter = yyyyMMdd HHmmThread Name= 3 formatter = yy-M-d ah:mmThread Name= 4 formatter = yy-M-d ah:mmThread Name= 5 default Formatter = yyyyMMdd HHmmThread Name= 5 formatter = yy-M-d ah:mmThread Name= 6 default Formatter = yyyyMMdd HHmmThread Name= 6 formatter = yy-M-d ah:mmThread Name= 7 default Formatter = yyyyMMdd HHmmThread Name= 7 formatter = yy-M-d ah:mmThread Name= 8 default Formatter = yyyyMMdd HHmmThread Name= 9 default Formatter = yyyyMMdd HHmmThread Name= 8 formatter = yy-M-d ah:mmThread Name= 9 formatter = yy-M-d ah:mmprivate static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue(){return new SimpleDateFormat("yyyyMMdd HHmm");}};public class Thread implements Runnable {......//與此線程有關(guān)的ThreadLocal值。由ThreadLocal類維護ThreadLocal.ThreadLocalMap threadLocals = null;?//與此線程有關(guān)的InheritableThreadLocal值。由InheritableThreadLocal類維護ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;......}public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value); }ThreadLocalMap getMap(Thread t) {return t.threadLocals; }

    ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal為key的鍵值對。這里解釋了為什么每個線程訪問同一個ThreadLocal,得到的確是不同的數(shù)值。另外,ThreadLocal 是 map結(jié)構(gòu)是為了讓每個線程可以關(guān)聯(lián)多個 ThreadLocal變量。
    通過上面這些內(nèi)容,我們足以通過猜測得出結(jié)論:最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。ThreadLocal類的set()方法
    從上面Thread類 源代碼可以看出Thread 類中有一個 threadLocals 和 一個inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實現(xiàn)的定制化的 HashMap。默認情況下這兩個變量都是null,只有當前線程調(diào)用 ThreadLocal 類的 set或get方法時才創(chuàng)建它們,實際上調(diào)用這兩個方法的時候,我們調(diào)用的是ThreadLocalMap類對應(yīng)的 get()、set()方法。
    從 Thread類源代碼入手。

    3.3. ThreadLocal原理
    上面有一段代碼用到了創(chuàng)建 ThreadLocal 變量的那段代碼用到了 Java8 的知識,它等于下面這段代碼,如果你寫了下面這段代碼的話,IDEA會提示你轉(zhuǎn)換為Java8的格式(IDEA真的不錯!)。因為ThreadLocal類在Java 8中擴展,使用一個新的方法withInitial(),將Supplier功能接口作為參數(shù)。
    從輸出中可以看出,Thread-0已經(jīng)改變了formatter的值,但仍然是thread-2默認格式化程序與初始化值相同,其他線程也一樣。
    Output:

    synchronized關(guān)鍵字和volatile關(guān)鍵字比較

    2.2. 說說 synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別

    說白了, volatile 關(guān)鍵字的主要作用就是保證變量的可見性然后還有一個作用是防止指令重排序。

    要解決這個問題,就需要把變量聲明為volatile,這就指示 JVM,這個變量是不穩(wěn)定的,每次使用它都到主存中進行讀取。

    在 JDK1.2 之前,Java的內(nèi)存模型實現(xiàn)總是從主存(即共享內(nèi)存)讀取變量,是不需要進行特別的注意的。而在當前的 Java 內(nèi)存模型下,線程可以把變量保存本地內(nèi)存比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。

    2.1. 講一下Java內(nèi)存模型

    2. volatile關(guān)鍵字

    ④ 性能已不是選擇標準

    如果你想使用上述功能,那么選擇ReentrantLock是一個不錯的選擇。

    • ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現(xiàn)這個機制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
    • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。
    • synchronized關(guān)鍵字與wait()和notify()/notifyAll()方法相結(jié)合可以實現(xiàn)等待/通知機制,ReentrantLock類當然也可以實現(xiàn),但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的靈活性,比如可以實現(xiàn)多路通知功能也就是在一個Lock對象中可以創(chuàng)建多個Condition實例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調(diào)度線程上更加靈活。 在使用notify()/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結(jié)合Condition實例可以實現(xiàn)“選擇性通知” ,這個功能非常重要,而且是Condition接口默認提供的。而synchronized關(guān)鍵字就相當于整個Lock對象中只有一個Condition實例,所有的線程都注冊在它一個身上。如果執(zhí)行notifyAll()方法的話就會通知所有處于等待狀態(tài)的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒注冊在該Condition實例中的所有等待線程。

    相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點:①等待可中斷;②可實現(xiàn)公平鎖;③可實現(xiàn)選擇性通知(鎖可以綁定多個條件)

    ③ ReentrantLock 比 synchronized 增加了一些高級功能

    synchronized 是依賴于 JVM 實現(xiàn)的,前面我們也講到了 虛擬機團隊在 JDK1.6 為 synchronized 關(guān)鍵字進行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機層面實現(xiàn)的,并沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現(xiàn)的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現(xiàn)的。

    ② synchronized 依賴于 JVM 而 ReentrantLock 依賴于 API

    兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖。

    ① 兩者都是可重入鎖

    1.5. 談?wù)?synchronized和ReentrantLock 的區(qū)別

    關(guān)于這幾種優(yōu)化的詳細信息可以查看:synchronized 關(guān)鍵字使用、底層原理、JDK1.6 之后的底層優(yōu)化以及 和ReenTrantLock 的對比

    鎖主要存在四種狀態(tài),依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。

    JDK1.6 對鎖的實現(xiàn)引入了大量的優(yōu)化,如偏向鎖、輕量級鎖、自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化等技術(shù)來減少鎖操作的開銷。

    1.4. 說說 JDK1.6 之后的synchronized 關(guān)鍵字底層做了哪些優(yōu)化,可以詳細介紹一下這些優(yōu)化嗎

    synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。

    synchronized關(guān)鍵字原理

    ② synchronized 修飾方法的的情況

    synchronized 同步語句塊的實現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。 當執(zhí)行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權(quán)。當計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。

    從上面我們可以看出:

    通過 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)境下也能正常運行。

    但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會出先問題,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實例。例如,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

  • 為 uniqueInstance 分配內(nèi)存空間
  • 初始化 uniqueInstance
  • 將 uniqueInstance 指向分配的內(nèi)存地址
  • uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分為三步執(zhí)行:

    另外,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要。


    總結(jié)

    以上是生活随笔為你收集整理的java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。