面試必看!花了三天整理出來的併發編程的鎖及內存模型,看完你就明白了!

前言

最近看到有很多粉絲私信我說,能不能給整理出一份面試的要點出來,說本身複習的時候思緒很亂,總是找不到重點。那麼今天就先給你們分享一個面試幾乎必問的點,併發!在面試中問的頻率很高的一個是分佈式,一個就是併發,具體乾貨都在下方了。java

面試環節

1. 面試官:你先說下你對synchronized的瞭解。

  • 我:synchronized能夠保證方法或者代碼在運行時,同一時刻只有一個方法能夠進入到臨界區,同時還能夠保證共享變量的內存可見性。面試

  • 我:Java中每一個對象均可以做爲鎖,這是synchronized實現同步的基礎: 一、普通同步方法:鎖是當前實例對象。
    二、靜態同步方法,鎖是當前類的class對象。
    三、同步代碼塊:鎖是括號裏的對象。

2. 面試官:當線程訪問同步代碼塊時,它首先要獲得鎖才能執行代碼,退出或者拋異常要釋放鎖,這是怎麼實現的呢?

  • 我:同步代碼塊是使用monitorenter和monitorexit指令實現的,同步方法依靠的是方法修飾符上的ACCSYNCHRONIZED實現的。
    一、同步代碼塊:monitorenter指令插入到同步代碼快的開始位置,monitorexit指令插入到同步代碼塊的結束位置,jVM保證每個monitorexist都有 一個monitorenter與之相對應。任何對應都有一個monitor與之相關聯,當且一個monitor被持有以後,他將處於鎖定狀態。線程執行到monitorenter指令 時,將會嘗試獲取對象對應的monitor全部權,即嘗試獲取對象的鎖。
    二、同步方法:synchronized方法是在Class文件的方法表中將該方法的accessflags字段中的synchronized標誌位置爲1,表示該方法是同步方法 並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass做爲鎖對象。

3. 面試官:你剛提到了每一個對象都有一個monitor與之對應,那什麼是Monitor呢?

  • 我:咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制,它一般被描述爲一個對象。與一切皆對象同樣,全部的java對象是天生的Monitor, 每個java對象都有成爲Monitor的潛質,由於在Java的設計中,每個java對象自打孃胎出來就帶了一把看不見的鎖,它被叫作內部鎖或者Monitor鎖。數據庫

  • 我:(接着說)Monitor是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象 都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中由一個Owner字段存放擁有該鎖的線程的惟一標識, 表示該鎖被這個線程佔用。

4. 面試官:很好。咱們知道synchronized是悲觀鎖,一直以來被當作重量級鎖。可是jdk1.6對鎖進行了優化,好比自旋鎖、適應性自旋鎖、鎖消除、偏向鎖以及 輕量級鎖等技術來減小鎖操做的開銷,這些你都瞭解嗎?

  • 我:知道一些。鎖主要存在四種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。他們會隨着競爭的激烈而逐漸升級。注意鎖能夠升級不可降級, 這種策略是爲了提升得到鎖和釋放鎖的效率。

5. 面試官:那你先來講下自旋鎖

  • 我:線程的阻塞和喚醒須要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU來講是一個負擔很重的工做,同時影響系統的併發能力,同時咱們發現不少應用上 對象鎖的鎖狀態只會持續很短的一段時間,爲了這一段很短的時間頻繁的阻塞和喚醒線程是不值得的,因此引入自旋鎖。何謂自旋鎖呢-就是讓線程等待一段時間,不會被 當即掛起,看持有鎖的線程是否會很快釋放鎖。那麼問題來了,等多長時間呢?時間短了等不到持有鎖的線程釋放鎖,時間長了佔用了處理器的時間,典型的「佔着茅坑不拉屎」, 反而帶來性能上的浪費。因此,自旋等待的時間(自旋)的次數必須有一個限度,若是自旋超過了定義的時間仍沒有得到鎖則要被掛起。

6. 面試官:我記得有個適應性自旋鎖,更加智能。你能說下麼?

  • 我:所謂自適應就意味着自旋的次數再也不是固定的,它是由上一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。線程若是自旋成功了,那麼下次自旋的次數 會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也可能成功。反之,若是對於某個鎖,不多有自旋能成功的,那麼之後等待這個鎖的時候自選的次數會減小甚至不自旋。 有了自適應自旋鎖,虛擬機對程序鎖的情況預測愈來愈準確,虛擬機會愈來愈聰明。

7. 面試官:給你看下面一段代碼,你說下會存在加鎖的操做嗎?

public static void main(String [] args) {
        Vector<String> vector = new Vector<>();
        for (int i=0; i<10; i++) {
            vector.add(i+"");
        }
        System.out.println(vector);
    }
  • 我:不會。這種狀況下,JVM檢測到不可能存在共享數據競爭,這時JVM會對這些同步鎖進行鎖消除。鎖消除的基礎是逃逸分析的數據支持。

8. 面試官:再看一段代碼,分析一下是在什麼地方加鎖的?

public static void test() {
        List<String> list = new ArrayList<>();
        for (int i=0; i<10; i++) {
            synchronized (Demo.class) {
                list.add(i + "");
            }
        }
        System.out.println(list);
    }
  • 我:雖然synchronized是在循環裏面,但實際上加鎖的範圍會擴大到循環外,這是鎖粗化。鎖粗化就是將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展 成一個範圍更大的鎖。

8. 面試官:你能說下輕量級鎖嗎?

  • 我:輕量級鎖提高程序同步性能的依據是:對於絕大部分的鎖,在整個同步週期內是不存在競爭的(區別於偏向鎖),這是一個經驗數據。若是沒有競爭,輕量級鎖使用CAS操做避免了使用互斥 量的開銷,但若是存在競爭,除了互斥量的開銷,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖比傳統的重量級鎖更慢。編程

  • 我接着說:輕量級鎖的加鎖過程是: 一、在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲對象目前的Mark Word的拷貝,官方稱之爲Displaced Mark Word,這時候線程堆棧與對象頭的狀態如圖:
    輕量級鎖的加鎖過程

面試必看!花了三天整理出來的併發編程的鎖及內存模型,看完你就明白了!

二、拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中。
三、拷貝成功後,虛擬機將使用CAS操做嘗試將鎖對象的Mark Word更新爲指向Lock Record的指針,並將線程棧幀中的Lock Record裏的owner指針指向Object的Mark Word。若是這個更新 動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,表示此對象處於輕量級鎖定狀態。緩存

四、若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明 多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌位的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。安全

9. 面試官:很詳細。那你能再解釋下偏向鎖嗎?

  • 我:偏向鎖的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。偏向鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘線程獲取,那持有 偏向鎖的線程將永遠不須要同步。數據結構

  • 我頓了下,接着說:當鎖第一次被線程獲取的時候,線程使用CAS操做把這個線程的ID記錄在對象Mark Word中,同時置偏向標誌位1.之後該線程在進入和退出代碼塊時不須要進行CAS操做 來加鎖和解鎖,只須要簡單測試一下對象頭的Mark Word裏是否存儲着指向當前線程的ID。若是測試成功,表示線程已經得到了鎖。當有另一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。 根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定或輕量級鎖定狀態。

10. 面試官:那偏向鎖、輕量級鎖和重量級鎖有什麼區別呢

  • 我:偏向鎖、輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。一個對象剛開始實例化的時候,沒有任何線程來訪問它時,它是可偏向的,意味着它認爲只可能有一個線程來訪問它,因此當第一個線程 訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操做,並將對象頭中的ThreadID改爲本身的Id,以後再訪問這個對象只須要對比ID。一旦有第二個線程訪問這個對象,由於偏向鎖不會釋放,因此第二個線程看到對象是偏向狀態,代表在這個對象上存在競爭了,檢查原來持有該對象的線程是否依然存活,若是掛了,則能夠將對象變爲無鎖狀態,而後從新偏向新的線程。若是原來的線程依然存活,則立刻執行那個線程的操做棧,檢查該對象的使用狀況,若是仍然須要持有偏向鎖,則偏向鎖升級爲輕量級鎖(偏向鎖就是此時升級爲輕量級鎖)。若是不存在使用了,則能夠將對象恢復成無鎖狀態,而後從新偏向。多線程

  • 我:(接着說)輕量級鎖認爲競爭存在,可是競爭的程度很輕,通常兩個線程對於同一個鎖的操做都會錯開,或者說自旋一下,另外一個線程就會釋放鎖。可是當自旋超過必定次數,或者一個線程持有鎖, 一個線程在自旋,又有第三個來訪,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程之外的線程都阻塞,防止CPU空轉。簡單的說就是:有競爭,偏向鎖升級爲輕量級鎖,競爭逐漸激烈, 輕量級鎖升級爲重量級鎖。

11.面試官:你瞭解java的內存模型嗎?能說下對JMM的理解嗎?

  • 我:在JSR113標準中有有一段對JMM的簡單介紹:Java虛擬機支持多線程執行。在Java中Thread類表明線程,建立一個線程的惟一方法就是建立一個Thread類的實例對象,當調用了對象的start方法後,相應的線程將會執行。線程的行爲有時會與咱們的直覺相左,特別是在線程沒有正確同步的狀況下。本規範描述了JMM平臺上多線程程序的語義,具體包含一個線程對共享變量的寫入什麼時候能被其餘線程看到。這是官方的接單介紹。併發

  • 我:Java內存模型是內存模型在JVM中的體現。這個模型的主要目標是定義程序中各個共享變量的訪問規則,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量這類的底層細節。經過這些規則來規範對內存的讀寫操做,保證了併發場景下的可見性、原子性和有序性。 JMM規定了多有的變量都存儲在主內存中,每條線程都有本身的工做內存,線程的工做內存保存了該線程中用到的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不是直接讀寫主內存。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間 進行數據同步。而JMM就做用於工做內存和主存之間數據同步過程。他規定了如何作數據同步以及何時作數據同步。也就是說Java線程之間的通訊由Java內存模型控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。app

  • 我:簡單的說:Java的多線程之間是經過共享內存進行通訊的,而因爲採用共享內存進行通訊,在通訊過程當中會存在一系列如原子性、可見性和有序性的問題。JMM就是爲了解決這些問題出現的, 這個模型創建了一些規範,能夠保證在多核CPU多線程編程的環境下,對共享變量的讀寫的原子性、可見性和有序性。

12. 面試官:那你說下Java內存模型的happens-before規則?

  • 我:在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。happens-before原則是JMM中很是重要的原則,它是判斷數據是否存在競爭、線程是否安全的主要依據,保證了多線程環境下的可見性。下面我說下happens-before的內容: happens-before的原則定義以下:
    一、若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
    二、兩個操做之間存在happens-before關係,並不必定意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。
    下面是happens-before的原則規則:
    一、程序次序規則:一個線程內,按照代碼書寫順序,書寫在前面的操做先行發生於書寫在後面的操做。
    二、鎖定規則:一個unLock操做先行發生於後面對同一個鎖的lock操做。
    三、volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做。
    四、傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C。
    五、線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個動做。
    六、線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
    七、線程終結規則:線程中全部的操做都先行發生於線程的終止檢測。
    八、對象終結規則:一個對象的初始化完成先行發生於它的finalize()方法的開始。

13 面試官:你剛纔提到了JVM會對咱們的程序進行重排序,那是隨便重排序嗎?

  • 我:不是的,它須要知足如下兩個條件:
    一、在單線程環境下不能改變程序運行的結果。
    二、存在數據依賴關係的不容許重排序。
    其實這兩點能夠歸結爲一點:沒法經過happens-before原則推導出來的,JMM容許任意的排序。

  • 我:這裏有必要提到as-if-serial語義:全部的操做均可覺得了優化而被重排序,可是你必須保證重排序後執行的結果不能被改變,編譯器、runtime和處理器都必須遵照 as-if-serial語義。注意as-if-serial只保證單線程環境,多線程環境下無效。舉個栗子:
int a=1; //A
int b=2; //B
int c=a+b; //C

A,B,C三個操做存在以下關係:A和B不存在數據依賴,A和C,B和C存在數據依賴,所以在重排序的時候:A和B能夠隨意排序,可是必須位於C的前面,但不管何種順序,最終結果C都是3.

  • 我接着說:下面舉個重排序對多線程影響的栗子:
public class RecordExample2 {
    int a = 0;
    boolean flag = false;

    /**
     * A線程執行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
     * B線程執行
     */
    public void read(){
        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }}

假如操做1和操做2之間重排序,可能會變成下面這種執行順序:
一、線程A執行flag=true;
二、線程B執行if(flag);
三、線程B執行int i = a+a;
四、線程A執行a=1。
按照這種執行順序線程B確定讀不到線程A設置的a值,在這裏多線程的語義就已經被重排序破壞了。操做3和操做4之間也能夠重排序,這裏就不闡述了。可是他們之間存在一個控制依賴的關係,由於只有操做3成立操做4纔會執行。當代碼中存在控制依賴性時,會影響指令序列的執行的並行度,因此編譯器和處理器會採用猜想執行來克服控制依賴對並行度的影響。假如操做3和操做4重排序了,操做4先執行,則先會把計算結果臨時保存到重排序緩衝中,當操做3爲真時纔會將計算結果寫入變量i中。

14. 面試官:你能給我講下對volatile的理解嗎?

  • 我:講volatile以前,先補充說明下Java內存模型中的三個概念:原子性、可見性和有序性

一、可見性:可見性是指線程之間的可見性,一個線程修改的狀態對另外一個線程是可見的。也就是一個線程的修改的結果,另外一個線程可以立刻看到。好比:用volatile修飾的變量,就會具備可見性,volatile修飾的變量不容許線程內部緩存和重排序,即直接修改內存,因此對其餘線程是可見的。但這裏要注意一個問題,volatile只能讓被他修飾的內容具備可見性,不能保證它具備原子性。好比 volatile int a=0; ++a;這個變量a具備可見性,可是a++是一個非原子操做,也就是這個操做一樣存在線程安全問題。在Java中,volatile/synchronized/final實現了可見性。

二、原子性:即一個操做或者多個操做要麼所有執行而且執行的過程不會被任何因素打斷,要麼都不執行。原子就像數據庫裏的事務同樣,他們是一個團隊,同生共死。看下面一個簡單的栗子:

i=0;  //1
j=i;  //2
i++; //3
i=j+1; //4

上面的四個操做,只有1是原子操做,其餘都不是原子操做。好比2包含了兩個操做:讀取i,將i值賦給j。在Java中synchronized/lock操做中保證原子性。

三、有序性:程序執行的順序按照代碼的前後順序執行。 前面JMM中提到了重排序,在java內存模型中,爲了效率是容許編譯器和處理器對指令進行重排序,並且重排序不會影響單線程的運行結果,可是對多線程有影響。Java中提供了volatile和synchronized保證有序性。

  • 我:volatile的原理是volatile能夠保證線程可見性且提供了必定的有序性,可是沒法保證原子性,在JVM底層volatile是採用「內存屏障」來實現的。總結起來就是: 一、保證可見性,不保證原子性。 二、禁止指令重排序。
  • 我:下面我來分析下volatile的這兩條性質。 volatile的內存語義是: 一、當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值當即刷新到主內存中。
    二、當讀一個volatile變量時,JMM會把線程的本地內存置爲無效,直接從主內存中讀取共享變量。 因此volatile的寫內存語義是直接刷新到主內存中,讀內存語義是直接從主內存中讀取---因此才能實現線程可見性。

那麼volatile的內存語義是如何實現的呢?對於通常的變量會被重排序,而對於volatile則不能,這樣會影響其內存語義,因此爲了實現volatile的內存語義JMM會限制重排序。
volatile的重排序規則
一、若是第一個操做爲volatile讀,則無論第二個操做是啥,都不能重排序。這個操做確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
二、當第二個操做爲volatile寫,則無論第一個操做是啥,都不能重排序。這個操做確保了volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
三、當第一個操做爲volatile寫,第二個操做爲volatile讀,不能重排序。

volatile的底層實現是經過插入內存屏障,可是對於編譯器來講,發現一個最優佈置來最小化插入內存屏障的總數幾乎是不可能的,因此JMM採用了保守策略。 以下:
一、在每個volatile寫操做前插入一個StoreStore屏障。
二、在每個volatile寫操做後插入一個StoreLoad屏障。
三、在每個volatile讀操做後插入一個LoadLoad屏障。
四、在每個volatile讀操做後插入一個LoadStore屏障。
總結:StoreStore屏障->寫操做->StoreLoad屏障->讀操做->LoadLoad屏障->LoadStore屏障。 下面經過一個例子簡單分析下: volatile原理分析

面試必看!花了三天整理出來的併發編程的鎖及內存模型,看完你就明白了!

15. 面試官:很好,看來你對volatile理解的挺深刻的了。咱們換個話題,你知道CAS嗎,能跟我講講嗎?

  • 我:CAS(Compare And Swap),比較並交換。整個AQS同步組件,Atomic原子類操做等等都是基於CAS實現的,甚至ConcurrentHashMap在JDK1.8版本中,也調整爲CAS+synchronized。 能夠說,CAS是整個JUC的基石。以下圖:

面試必看!花了三天整理出來的併發編程的鎖及內存模型,看完你就明白了!

  • 我:CAS的實現方式其實不難。在CAS中有三個參數:內存值V、舊的預期值A、要更新的值B,當且僅當內存值V的值等於舊的預期值A時,纔會將內存值V的值修改成B,不然什麼也不幹,是一種樂觀鎖。 其僞代碼以下:
if (this.value == A) {
    this.value = B
    return true;
} else {
    return false;
}
  • 我:接着我舉了個AtomicInteger的例子,來給面試官闡述CAS的實現。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

如上是AtomicInteger的源碼: 一、Unsafe是CAS的核心類,Java沒法直接訪問底層操做系統,而是經過本地native方法訪問。不過儘管如此,JVM仍是開了個後門:Unsafe,它提供了 硬件級別的原子操做。
二、valueOffset:爲變量值在內存中的偏移地址,Unsafe就是經過偏移地址來獲得數據的原值的。
三、value:當前值,使用volatile修飾,保證多線程環境下看見的是同一個。

// AtomicInteger.java
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

// Unsafe.java
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;
}

在方法compareAndSwapInt(var1, var2, var5, var5 + var4)中,有四個參數,分別表明:對象,對象的地址,預期值,修改值。

  • 我:CAS能夠保證一次的讀-改-寫操做是原子操做,在單處理器上該操做容易實現,可是在多處理器上實現就有點複雜。CPU提供了兩種方法來實現多處理器的原子操做:總線加鎖或者緩存加鎖。
    一、總線加鎖:總線加鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔使用共享內存。可是這種處理方式顯然 有點霸道。
    二、緩存加鎖:其實針對上面的狀況,咱們只須要保證在同一時刻,對某個內存地址的操做是原子性的便可。緩存加鎖,就是緩存在內存區域的數據若是在加鎖期間,當它執行鎖操做寫回內存時, 處理器再也不輸出#LOCK信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制能夠保證同一個內存區域的數據僅能被一個處理器修改,也就是說當CPU1修改緩存行 中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。

16. 面試官:那CAS有什麼缺陷嗎?

  • 我:CAS雖然高效的解決了原子問題,可是仍是存在一些缺陷的,主要體如今三個方面:
    一、循環時間太長:若是自旋CAS長時間不成功,則會給CPU帶來很是大的開銷,在JUC中,有些地方就會限制CAS自旋的次數。
    二、只能保證一個共享變量原子操做:看了CAS的實現就知道這隻能針對一個共享變量,若是是多個共享變量就只能使用鎖了。或者把多個變量整成一個變量也能夠用CAS。
    三、ABA問題:CAS須要檢查操做值有沒有發生改變,若是沒有發生改變則更新,可是存在這樣一種狀況:若是一個值原來是A,變成了B,而後又變成了A,那麼在CAS檢查的時候會發現沒有改變,可是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題的解決方案是加上版本號,即在每一個變量都加上一個版本號,每次改變時加1,即A->B->A,變成1A->2B->3A。例如原子類中AtomicInteger會發生ABA問題,使用AtomicStampedReference能夠解決ABA問題。

結語

其實在面試裏,多線程,併發這塊問的仍是很是頻繁的,你們看完以後有什麼不懂的歡迎在評論區討論,也能夠私信問我,通常我看到以後都會回的!

相關文章
相關標籤/搜索