java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析
版權說明:本文內容根據 github 開源項目整理所得
項目地址:
https://github.com/Snailclimb/JavaGuide?github.com一、基礎
什么是線程和進程?
何為進程?
進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。
在 Java 中,當我們啟動 main 函數時其實就是啟動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。
如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。
何為線程?
線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區資源,但每個線程有自己的程序計數器、虛擬機棧和本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。
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 // 分發處理給 JVM 信號的線程[3] Finalizer //調用對象 finalize 方法的線程[2] Reference Handler //清除 reference 線程[1] main //main 線程,程序入口如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。
多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。
認識線程死鎖
什么是線程死鎖?如何避免死鎖?
Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換會這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
多線程編程中一般線程的個數都大于 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 采取的策略是為每個線程分配時間片并輪轉的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態讓給其他線程使用,這個過程就屬于一次上下文切換。
什么是上下文切換?
當線程執行 wait()方法之后,線程進入 **WAITING(等待)**狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當于在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之后將會進入到 TERMINATED(終止) 狀態。
RUNNABLE-VS-RUNNING
操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系統一般將這兩個狀態統稱為 RUNNABLE(運行中) 狀態 。由上圖可以看出:線程創建之后它將處于 NEW(新建) 狀態,調用 start() 方法后開始運行,線程這時候處于 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)后就處于 RUNNING(運行) 狀態。
Java 線程狀態變遷
線程在生命周期中并不是固定處于某一個狀態而是隨著代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 并發編程藝術》4.1.4 節):
Java 線程的狀態
Java 線程在運行的生命周期中的指定時刻只可能處于下面 6 種不同狀態的其中一個狀態(圖源《Java 并發編程藝術》4.1.4 節)。
說說線程的生命周期和狀態?
并發編程的目的就是為了能提高程序的執行效率提高程序運行速度,但是并發編程并不總是能提高程序運行速度的,而且并發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖還有受限于硬件和軟件的資源閑置問題。
使用多線程可能帶來什么問題?
- 單核時代: 在單核時代多線程主要是為了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閑;進行 IO 操作時,CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
- 多核時代: 多核時代多線程主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個復雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。
再深入到計算機底層來探討:
- **從計算機底層來說:**線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小于進程。另外,多核 CPU 時代意味著多個線程可以同時運行,這減少了線程上下文切換的開銷。
- **從當代互聯網發展趨勢來說:**現在的系統動不動就要求百萬級甚至千萬級的并發量,而多線程并發編程正是開發高并發系統的基礎,利用好多線程機制可以大大提高系統整體的并發能力以及性能。
先從總體上來說:
為什么要使用多線程呢?
- 并發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);
- **并行:**單位時間內,多個任務同時執行。
說說并發與并行的區別?
堆和方法區是所有線程共享的資源,其中堆是進程中最大的一塊內存,主要用于存放新創建的對象 (所有對象都在這里分配內存),方法區主要用于存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
一句話簡單了解堆和方法區
所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
- **虛擬機棧:**每個 Java 方法在執行的同時會創建一個棧幀用于存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應著一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
- **本地方法棧:**和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。
虛擬機棧和本地方法棧為什么是私有的?
所以,程序計數器私有主要是為了線程切換后能恢復到正確的執行位置。
需要注意的是,如果執行的是 native 方法,那么程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的才是下一條指令的地址。
程序計數器主要有下面兩個作用:
程序計數器為什么是私有的?
下面來思考這樣一個問題:為什么程序計數器、虛擬機棧和本地方法棧是線程私有的呢?為什么堆和方法區是線程共享的呢?
下面是該知識點的擴展內容!
總結: 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在于基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利于資源的管理和保護;而進程正相反
從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的堆和方法區 (JDK1.8 之后的元空間)資源,但是每個線程有自己的程序計數器、虛擬機棧 和 本地方法棧。
下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關系。如果你對 Java 內存區域 (運行時數據區) 這部分知識不太了解的話可以閱讀一下這篇文章:《可能是把 Java 內存區域講的最清楚的一篇文章》
圖解進程和線程的關系
從 JVM 角度說進程和線程之間的關系
請簡要描述線程與進程的關系,區別及優缺點?
從上面的輸出內容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行。
上述程序輸出如下(輸出內容可能不同,不用太糾結下面每個線程的作用,只用知道 main 線程執行 main 方法即可):
二、進階考點
1. synchronized 關鍵字
1.1. 說一說自己對于 synchronized 關鍵字的了解
synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。
另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
1.2. 說說自己是怎么使用 synchronized 關鍵字,在項目中用到了嗎
synchronized關鍵字最主要的三種使用方式:
- 修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
- 修飾靜態方法: :也就是給當前類加鎖,會作用于類的所有對象實例,因為靜態成員不屬于任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份)。所以如果一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法占用的鎖是當前實例對象鎖。
- 修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。盡量不要使用 synchronized(String a) 因為JVM中,字符串常量池具有緩存功能!
下面我以一個常見的面試題為例講解一下 synchronized 關鍵字的具體使用。
面試中面試官經常會說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單例模式的原理唄!”
雙重校驗鎖實現對象單例(線程安全)
public class Singleton {?private volatile static Singleton uniqueInstance;?private Singleton() {}?public static Singleton getUniqueInstance() {//先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼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關鍵字是線程同步的輕量級實現,所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用于變量而synchronized關鍵字可以修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之后執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些。
- 多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
- volatile關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized關鍵字兩者都能保證。
- volatile關鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized關鍵字解決的是多個線程之間訪問資源的同步性。
3. ThreadLocal
3.1. ThreadLocal簡介
通常情況下,我們創建的變量是可以被任何一個線程訪問并修改的。如果想實現每一個線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。
如果你創建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認值或將其值更改為當前線程所存的副本的值,從而避免了線程安全問題。
再舉個簡單的例子:
比如有兩個人去寶屋收集寶物,這兩個共用一個袋子的話肯定會產生爭執,但是給他們兩個人每個人分配一個袋子的話就不會出現這樣的問題。如果把這兩個人比作線程的話,那么ThreadLocal就是用來這兩個線程競爭的。
3.2. ThreadLocal示例
相信看了上面的解釋,大家已經搞懂 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 {......//與此線程有關的ThreadLocal值。由ThreadLocal類維護ThreadLocal.ThreadLocalMap threadLocals = null;?//與此線程有關的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的靜態內部類。每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal為key的鍵值對。這里解釋了為什么每個線程訪問同一個ThreadLocal,得到的確是不同的數值。另外,ThreadLocal 是 map結構是為了讓每個線程可以關聯多個 ThreadLocal變量。
通過上面這些內容,我們足以通過猜測得出結論:最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。ThreadLocal類的set()方法
從上面Thread類 源代碼可以看出Thread 類中有一個 threadLocals 和 一個inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實現的定制化的 HashMap。默認情況下這兩個變量都是null,只有當前線程調用 ThreadLocal 類的 set或get方法時才創建它們,實際上調用這兩個方法的時候,我們調用的是ThreadLocalMap類對應的 get()、set()方法。
從 Thread類源代碼入手。
3.3. ThreadLocal原理
上面有一段代碼用到了創建 ThreadLocal 變量的那段代碼用到了 Java8 的知識,它等于下面這段代碼,如果你寫了下面這段代碼的話,IDEA會提示你轉換為Java8的格式(IDEA真的不錯!)。因為ThreadLocal類在Java 8中擴展,使用一個新的方法withInitial(),將Supplier功能接口作為參數。
從輸出中可以看出,Thread-0已經改變了formatter的值,但仍然是thread-2默認格式化程序與初始化值相同,其他線程也一樣。
Output:
synchronized關鍵字和volatile關鍵字比較
2.2. 說說 synchronized 關鍵字和 volatile 關鍵字的區別
說白了, volatile 關鍵字的主要作用就是保證變量的可見性然后還有一個作用是防止指令重排序。
要解決這個問題,就需要把變量聲明為volatile,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。
在 JDK1.2 之前,Java的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而在當前的 Java 內存模型下,線程可以把變量保存本地內存比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。
2.1. 講一下Java內存模型
2. volatile關鍵字
④ 性能已不是選擇標準
如果你想使用上述功能,那么選擇ReentrantLock是一個不錯的選擇。
- ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
- ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
- synchronized關鍵字與wait()和notify()/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),線程對象可以注冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用notify()/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現“選擇性通知” ,這個功能非常重要,而且是Condition接口默認提供的。而synchronized關鍵字就相當于整個Lock對象中只有一個Condition實例,所有的線程都注冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處于等待狀態的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒注冊在該Condition實例中的所有等待線程。
相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以綁定多個條件)
③ ReentrantLock 比 synchronized 增加了一些高級功能
synchronized 是依賴于 JVM 實現的,前面我們也講到了 虛擬機團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,并沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。
② synchronized 依賴于 JVM 而 ReentrantLock 依賴于 API
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
① 兩者都是可重入鎖
1.5. 談談 synchronized和ReentrantLock 的區別
關于這幾種優化的詳細信息可以查看:synchronized 關鍵字使用、底層原理、JDK1.6 之后的底層優化以及 和ReenTrantLock 的對比
鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。
1.4. 說說 JDK1.6 之后的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。
synchronized關鍵字原理
② synchronized 修飾方法的的情況
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權。當計數器為0則可以成功獲取,獲取后將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令后,將鎖計數器設為0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
從上面我們可以看出:
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo.class。
① synchronized 同步語句塊的情況
synchronized 關鍵字底層原理屬于 JVM 層面。
1.3. 講一下 synchronized 關鍵字的底層原理
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 后發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
uniqueInstance 采用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行:
另外,需要注意 uniqueInstance 采用 volatile 關鍵字修飾也是很有必要。
總結
以上是生活随笔為你收集整理的java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 5.6 5.7不兼容_同一条
- 下一篇: c语言位运算+乘法,关于c语言中的位运算