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

歡迎訪問 生活随笔!

生活随笔

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

java

面试必会系列 - 1.5 Java 锁机制

發(fā)布時間:2024/2/28 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试必会系列 - 1.5 Java 锁机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文已收錄至 github,完整圖文:https://github.com/HanquanHq/MD-Notes
面試必會系列專欄:https://blog.csdn.net/sinat_42483341/category_10300357.html

Java 鎖機制

概覽

  • syncronized 鎖升級過程
  • ReentrantLock 可重入鎖
  • volatile 關(guān)鍵字
  • JUC 包下新的同步機制

syncronized

給一個變量/一段代碼加鎖,線程拿到鎖之后,才能修改一個變量/執(zhí)行一段代碼

  • wait()
  • notify()

synchronized 關(guān)鍵字可以作用于 方法 或者 代碼塊,最主要有以下幾種使用方式:

注意:

  • 不要用 String 類型的常量作為鎖(兩個不同的變量,指向相同的String常量,作為鎖的時候,可能造成沖突)
  • 不要用 Integer,Long 類型的變量作為鎖(內(nèi)部做了特殊處理,改變其值時,可能會變成新對象)
  • syncronized 實現(xiàn)原理?

    Object o = new Object(); synchronized (o) {}

    添加 synchronized 之后,生成的 .class 字節(jié)碼:

    0 new #2 <java/lang/Object>3 dup4 invokespecial #1 <java/lang/Object.<init>>7 astore_18 aload_19 dup 10 astore_2 11 monitorenter // 獲取鎖 12 aload_2 13 monitorexit // 釋放鎖 14 goto 22 (+8) 17 astore_3 18 aload_2 19 monitorexit // 兜底:如果發(fā)生異常,自動釋放鎖 20 aload_3 21 athrow 22 return
    1、字節(jié)碼層面:ACC_SYNCHRONIZED,monitorenter,monitorexit(重量級鎖)

    事實上,只有在JDK1.6之前,synchronized的實現(xiàn)才會直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。從JDK6開始,HotSpot虛擬機開發(fā)團隊對Java中的鎖進行優(yōu)化,如增加了適應(yīng)性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等優(yōu)化策略。

    https://juejin.im/post/6844903918653145102

    ACC_SYNCHRONIZED:把 syncronized 加在方法上時的字節(jié)碼

    方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有 monitor(虛擬機規(guī)范中用的是管程一詞),然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor

    ACC_SYNCHRONIZED:

    方法級別的同步是隱式的,作為方法調(diào)用的一部分。同步方法的常量池中會有一個ACC_SYNCHRONIZED標志。當調(diào)用一個設(shè)置了ACC_SYNCHRONIZED標志的方法,執(zhí)行線程需要先獲得monitor鎖,然后開始執(zhí)行方法,方法執(zhí)行之后再釋放monitor鎖,當方法不管是正常return還是拋出異常都會釋放對應(yīng)的monitor鎖。在這期間,如果其他線程來請求執(zhí)行方法,會因為無法獲得監(jiān)視器鎖而被阻斷住。如果在方法執(zhí)行過程中,發(fā)生了異常,并且方法內(nèi)部并沒有處理該異常,那么在異常被拋到方法外面之前監(jiān)視器鎖會被自動釋放。

    monitorenter, monitorexit:把 syncronized 用于對象時的字節(jié)碼

    代碼塊的同步是利用 monitorenter 和 monitorexit 這兩個字節(jié)碼指令。它們分別位于同步代碼塊的開始和結(jié)束位置。當 JVM 執(zhí)行到 monitorenter 指令時,當前線程試圖獲取 monitor 對象的所有權(quán)。鎖重入的原理:

    • 如果未加鎖或者已經(jīng)被當前線程所持有,把鎖計數(shù)器 +1
    • 當執(zhí)行 monitorexit 指令時,鎖計數(shù)器 -1
    • 當鎖計數(shù)器為 0 時,鎖被釋放
    • 如果獲取monitor對象失敗,該線程進入阻塞狀態(tài),直到其他線程釋放鎖。
    monitorenter:

    “它的實現(xiàn)在 hotspot 源碼的 interpreterRuntime.cpp 中,在 monitorenter 函數(shù)內(nèi)部的實現(xiàn)為:如果打開了偏向鎖,則進入 fast_enter, 在 safepoint情況下,嘗試獲取偏向鎖,成功則返回,失敗則進入 slow_enter, 升級為自旋鎖,如果自旋鎖失敗,則膨脹 inflate 成為重量級鎖。重量級鎖的代碼在 syncronizer.cpp 中,里面調(diào)用了 linux 內(nèi)核的一些實現(xiàn)方法。

    每個對象都與一個 monitor 相關(guān)聯(lián)。當且僅當擁有所有者時(被擁有),monitor才會被鎖定。執(zhí)行到monitorenter指令的線程,會嘗試去獲得對應(yīng)的 monitor,如下:

    每個對象維護著一個記錄著被鎖次數(shù)的計數(shù)器, 對象未被鎖定時,該計數(shù)器為0。線程進入monitor(執(zhí)行monitorenter 指令)時,會把計數(shù)器設(shè)置為1。當同一個線程再次獲得該對象的鎖的時候,計數(shù)器再次自增,這就是鎖重入。當其他線程想獲得該 monitor 的時候,就會阻塞,直到計數(shù)器為0才能成功。

    monitorexit:

    monitor 的擁有者線程才能執(zhí)行 monitorexit 指令。線程執(zhí)行monitorexit指令,就會讓monitor的計數(shù)器減一。如果計數(shù)器為0,表明該線程不再擁有monitor。其他線程就允許嘗試去獲得該monitor了。

    monitor 監(jiān)視器

    monitor 是什么? 它可以理解為一種同步工具,或者說是同步機制,它通常被描述成一個對象。操作系統(tǒng)的管程 是概念原理,在 HotSpot 中,Monitor(管程)是由 ObjectMonitor 實現(xiàn)的。

    Java Monitor 的工作機理如圖所示:

    對象是如何跟 monitor 關(guān)聯(lián)的呢?直接看圖:

    對象里有對象頭,對象頭里面有 markmord,markmord 指針指向了 monitor

    2、JVM 層面
    • C, C++ 調(diào)用了操作系統(tǒng)提供的同步機制,在 win 和 linux 上不同
    3、OS 和硬件層面
    • X86 : lock cmpxchg / xxx
    • lock 是處理多處理器之間的總線鎖問題

    syncronized 鎖升級過程

    早期(JDK 1.2 以前)syncronized 都是重量級鎖,向操作系統(tǒng)申請鎖。后來進行了優(yōu)化,有了鎖升級過程:

    偏向鎖、自旋鎖都是用戶空間完成,JVM自己管理;重量級鎖需要向內(nèi)核申請

    markword 組成

    偏向鎖

    • 普通對象加了 syncronized,會加上偏向鎖。偏向鎖默認是打開的,但是有一個時延,如果要觀察到偏向鎖,應(yīng)該設(shè)定參數(shù)

    • **為什么要有偏向鎖?**我們知道,Vector,StringBuffer 都有很多使用了 syncronized 的同步方法,但是在工業(yè)實踐中,我們通常是在單線程的時候使用它的,**沒有必要 **設(shè)計 鎖競爭機制。為了在沒有競爭的情況下減少鎖開銷,偏向鎖偏向于第一個獲得它的線程,把第一個訪問的 線程 id(在C++實現(xiàn)中叫線程指針) 寫到 markword 中,而不去真正加鎖。如果一直沒有被其他線程競爭,則持有偏向鎖的線程將不需要進行同步。

      默認情況,偏向鎖有個時延,默認是4秒。why? 因為 JVM 虛擬機自己有一些默認啟動的線程,里面有好多sync代碼,明確知道這些sync代碼啟動時會有競爭,如果使用偏向鎖,就會造成偏向鎖不斷的進行鎖撤銷和鎖升級的操作,效率較低,所以默認偏向鎖啟動延時 4s。

      -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 // 設(shè)置偏向鎖0s立刻啟動

      設(shè)定上述參數(shù),new Object () - > 101 偏向鎖 -> 線程ID為0 -> 匿名偏向 Anonymous BiasedLock ,指還沒有偏向任何一個線程。打開偏向鎖,new出來的對象,默認就是一個可偏向匿名對象 101(或者 sleep 5000 之后再打印也可以看到偏向鎖)

    輕量級鎖(也叫 自旋鎖 / 無鎖 / CAS)

    • 偏向鎖時,有其他線程來競爭鎖,則先把 偏向鎖撤銷,然后進行 自旋鎖(輕量級鎖)競爭

    • 在沒有競爭的前提下,減少 重量級鎖使用操作系統(tǒng) mutex 互斥量 產(chǎn)生的性能消耗

      JVM虛擬機在每一個競爭線程的棧幀中,建立一個自己的 **鎖記錄 (Lock Record, LR) **空間,存儲鎖對象目前 markword 的拷貝。競爭線程 使用 CAS 的方式,嘗試把被競爭對象的 markword 更新為指向競爭線程 LR 的指針,如果更新成功即代表該線程擁有了鎖,鎖標志位將轉(zhuǎn)變?yōu)?00,表示處于輕量級鎖定狀態(tài)。

    • CAS 是一種樂觀鎖:cas(v, a, b) 變量v,期待a,修改值b

    • Java 中調(diào)用了 native 的 compareAndSwapXXX() 方法

    • 每個人在自己的線程內(nèi)部生成一個自己LR(Lock Record鎖記錄),兩個線程通過自己的方式嘗試將 LR 寫門上,競爭成功的開始運行,競爭失敗的一直自旋等待。

    • 實際上是匯編指令 lock cmpxchg,硬件層面實現(xiàn):在操作過程中不允許被其他CPU打斷,避免CAS在寫數(shù)據(jù)的時候被其他線程打斷,相比操作系統(tǒng)級別的鎖,效率要高很多。

      LOCK 本身不是一個指令:它是一個指令前綴,該指令必須是對存儲器( INC, XCHG, CMPXCHG等)進行 讀 – 修改 – 寫操作的指令,在這種情況下,它是在所保存的地址處包含字的incl (%ecx)指令在ecx寄存器中。

      LOCK 前綴確保CPU在操作期間擁有適當?shù)腸aching行的獨占所有權(quán),并提供某些額外的訂購保證。 這可以通過聲明一個總線鎖來實現(xiàn),但是CPU將盡可能地避免這種情況。

      CPU在執(zhí)行cmpxchg指令之前會執(zhí)行l(wèi)ock鎖定總線,實際是鎖定北橋信號。現(xiàn)在的主板貌似沒有南北橋了,集成到cpu里面了 http://www.360doc.com/content/18/0216/10/44130189_730197818.shtml

    • 如何解決ABA問題?

      • 基礎(chǔ)數(shù)據(jù)類型即使出現(xiàn)了ABA,一般問題不大。
      • 解決方式:加版本號,后面檢查的時候連版本號一起檢查。
      • Atomic里面有個帶版本號的類 AtomicStampedReference,目前還沒有人在面試的時候遇到過。
    • 線程始終得不到鎖會自旋消耗 CPU

    重量級鎖

    • 輕量級鎖再競爭,升級為重量級鎖
      • 重量級鎖向 Linux 內(nèi)核 申請鎖 mutex, CPU從3級-0級系統(tǒng)調(diào)用,線程掛起,進入等待隊列,等待操作系統(tǒng)的調(diào)度,然后再映射回用戶空間。重量級鎖有一個 waitset 等待隊列,不需要 CAS 消耗 CPU 時間。由操作系統(tǒng)的 CFS 公平調(diào)度策略來調(diào)度。
    • 在 markword 中記錄 ObjectMonitor,是 JVM 用 C++ 寫的一個 Object,需要向操作系統(tǒng)申請鎖,詳細過程你去讀 HotSpot 的 interpreterRuntime.cpp 的 monitorenter方法

    自旋鎖,什么時候升級為重量級鎖?

    競爭加劇:有線程超過10次自旋, -XX:PreBlockSpin, 或者自旋線程數(shù)超過CPU核數(shù)的一半, 1.6之后,加入自適應(yīng)自旋 Adapative Self Spinning , JVM自己控制自旋次數(shù),不需要你設(shè)置參數(shù)了。所以你在做實驗的時候,會發(fā)現(xiàn)有時候 syncronized 并不比 AtomicInteger 效率低。

    升級重量級鎖:-> 向操作系統(tǒng)申請資源,linux mutex , CPU從3級-0級系統(tǒng)調(diào)用,線程掛起,進入等待隊列,等待操作系統(tǒng)的調(diào)度,然后再映射回用戶空間

    為什么有自旋鎖,還需要重量級鎖?

    自旋是消耗CPU資源的,如果鎖的時間長,或者自旋線程多,CPU會被大量消耗

    重量級鎖有等待隊列,所有拿不到鎖的進入等待隊列,不需要消耗CPU資源

    偏向鎖,是否一定比自旋鎖效率高?

    不一定,在明確知道會有多線程競爭的情況下,偏向鎖肯定會涉及鎖撤銷,這時候直接使用自旋鎖

    例如,JVM 啟動過程,會有很多線程競爭(明確知道,比如在剛啟動的之后,肯定有很多線程要爭搶內(nèi)存的位置),所以,默認情況啟動時不打開偏向鎖,過一段兒時間再打開。

    鎖重入

    sychronized是可重入鎖

    重入次數(shù)必須記錄,因為要解鎖幾次必須得對應(yīng)

    偏向鎖、自旋鎖,重入次數(shù)存放在線程棧,讓 LR + 1

    重量級鎖 -> ? ObjectMonitor 字段上

    如果計算過對象的 hashCode,則對象無法進入偏向狀態(tài)!

    輕量級鎖重量級鎖的hashCode存在與什么地方?

    答案:線程棧中,輕量級鎖的LR中,或是代表重量級鎖的ObjectMonitor的成員中

    ReentrantLock 可重入鎖

    ReentrantLock 的使用

    ReentrantLock 和 synchronized 都是可重入鎖,Reentrantlock 可以完成 synchronized 同樣的功能:由于 m1 鎖定 this,只有 m1 執(zhí)行完畢的時候,m2 才能執(zhí)行。

    • 使用 Reentrantlock,可以進行 tryLock “嘗試鎖定”,這樣如果無法鎖定,或者在指定時間內(nèi)無法鎖定,線程可以決定是否繼續(xù)等待。

    • 使用 ReentrantLock 還可以調(diào)用 lockInterruptibly 方法,可以對線程 interrupt 方法做出響應(yīng),在一個線程等待鎖的過程中,可以被打斷。

    • ReentrantLock 還可以指定為公平鎖,但是效率偏低。

    • 需要注意的是,使用 syncronized 鎖定的話,如果遇到異常,JVM 會自動釋放鎖,但是 ReentrantLock 必須 手動釋放鎖,因此經(jīng)常在 finally 中保證鎖的 unlock 釋放。

    https://blog.csdn.net/fuyuwei2015/article/details/83719444 ReentrantLock 原理

    public class TestLock {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newCachedThreadPool(); // 線程池ReentrantLock reentrantLock = new ReentrantLock();int count[] = {0};for (int i = 0; i < 10000; i++) {executorService.submit(() -> {try {reentrantLock.lock(); // 獲取鎖count[0]++;} catch (Exception e) {e.printStackTrace();} finally {reentrantLock.unlock(); // 釋放鎖}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.HOURS);System.out.println(count[0]); // 10000} }

    private Lock lock = new ReentrantLock()

    • lock.lock() 獲取鎖
    • lock.unlock() 釋放鎖

    ReentrantLock 主要利用 CAS + AQS(AbstractQueuedSynchronizer) 來實現(xiàn)。

    lock() 與 unlock() 實現(xiàn)原理

    可重入鎖

    可重入鎖是指,同一個線程可以多次獲取同一把鎖。ReentrantLock 和 synchronized 都是可重入鎖。

    可中斷鎖

    可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應(yīng)中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。

    公平鎖與非公平鎖

    公平鎖是指,多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到順序,非公平鎖則允許線程“插隊”。

    synchronized是非公平鎖,而ReentrantLock的默認實現(xiàn)是非公平鎖,但是也可以設(shè)置為公平鎖。

    ReentrantLock提供了兩個構(gòu)造器,可以指定使用公平鎖和非公平鎖。分別是:

    public ReentrantLock() {sync = new NonfairSync(); }public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }

    默認初始化為 NonfairSync 對象,即非公平鎖。由 lock() 和 unlock() 的源碼可以看到,它們只是分別調(diào)用了 sync.acquire(1); 和 sync.release(1); 方法。

    Test.java
    reentrantLock.lock();
    ReentrantLock.java
    private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {...}public void lock() {sync.acquire(1); }
    AbstractQueuedSynchronizer.java
    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }
    CAS 操作 (CompareAndSwap)

    CAS操作簡單的說就是比較并交換。CAS 操作包含三個操作數(shù):內(nèi)存位置(V)、預(yù)期原值(A)、新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。

    CAS 有效地說明了:我認為位置 V 應(yīng)該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。

    Java并發(fā)包 java.util.concurrent 中大量使用了 CAS 操作,涉及到并發(fā)的地方都調(diào)用了 sun.misc.Unsafe 類方法進行CAS操作。

    [外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Z66yxdnx-1604373776698)(images/image-20200806110142620.png)]

    syncronized 和 ReentrantLock 使用哪個?怎么選擇?

    要根據(jù)場景選擇,因為 syncronized 的鎖升級是不可逆的,所以如果在一個系統(tǒng)中,某一時刻的訪問量比較大的話,升級為重量級鎖,并且不能撤銷,這樣在普通流量下,效率會變差。所以如果你的 QPS 比較穩(wěn)定的話,推薦使用 syncronized,你不需要去手動加鎖、釋放鎖。

    volatile

    volatile 作用

    一個線程中的改變,在另一個線程中可以立刻看到。

    • 保證線程的可見性(但是不能保證原子性,原子性需要加 syncronized)
    • 禁止指令的重排序

    什么是指令重排序?

    為了提高性能,編譯器和處理器通常會對指令進行重排序,重排序指從源代碼到指令序列的重排序,分為三種:

    ① 編譯器優(yōu)化的重排序,編譯器 在不改變單線程程序語義的前提下可以重排語句的執(zhí)行順序。

    ② 指令級并行的重排序,如果不存在數(shù)據(jù)依賴性,處理器 可以改變語句對應(yīng)機器指令的執(zhí)行順序。

    ③ 內(nèi)存系統(tǒng)的重排序。

    DCL 單例要不要加 volitile?

    需要。為了防止指令重排序?qū)е履玫?半初始化 的變量。只有在超高并發(fā)的時候才有可能測出來。實際上我們要單例的時候,通常直接交由 spring 去管理。

    單例模式代碼
    public class SingleInstance {private SingleInstance() {}private static SingleInstance INSTANCE;public static SingleInstance getInstance() {if (INSTANCE == null) {synchronized (SingleInstance.class) {if (INSTANCE == null) { // Double Check LockINSTANCE = new SingleInstance();}}}return INSTANCE;} }
    new 對象過程的字節(jié)碼

    使用 INSTANCE = new SingleInstance() 單條語句創(chuàng)建實例對象時,編譯后形成的指令,并不是一個原子操作,可能被切換到另外的線程打斷。它是分三步來完成的:

    0 new #2 <T> // 申請內(nèi)存 3 dup 4 invokespecial #3 <T.<init>> // 構(gòu)造方法進行初始化,成員變量賦【默認值】 7 astore_1 // 成員變量賦【初始值】 8 return 
  • 創(chuàng)建內(nèi)存空間。
  • 執(zhí)行構(gòu)造函數(shù),成員變量初始化(init)。此時成員變量并沒有賦初值,而是默認值 0
  • 將 INSTANCE 引用指向分配的內(nèi)存空間
  • JVM 為了優(yōu)化指令,允許指令重排序,有可能按照 1 –> 3 –> 2 步驟來執(zhí)行。當線程 a 執(zhí)行步驟 3 完畢,在執(zhí)行步驟 2 之前,被切換到線程 b 上,這時候 INSTANCE 判斷為非空,此時線程 b 直接來到 return instance 語句,拿走 INSTANCE 然后使用,導(dǎo)致拿到半初始化的變量。

    硬件和 JVM 如何保證特定情況下不亂序?

    https://www.jianshu.com/p/64240319ed60 一文解決內(nèi)存屏障

    1、硬件 CPU 層面(針對 x86 CPU)

    • sfence(store fence)指令:在 sfence 指令前的寫操作,必須在 sfence 指令后的寫操作前完成。
    • lfence(load fence)指令:在 lfence 指令前的讀操作,必須在 lfence 指令后的讀操作前完成。
    • mfence(mixed fence)指令:讀寫屏障,mfence指令實現(xiàn)了Full Barrier,相當于StoreLoad Barriers

    原子指令,如x86上的lock … 指令是一個 Full Barrier,執(zhí)行時會鎖住內(nèi)存子系統(tǒng)來確保執(zhí)行順序,甚至跨多個CPU。Software Locks 通常使用了內(nèi)存屏障原子指令來實現(xiàn)變量可見性保持程序順序.

    2、JVM 層面(JSR133)

    • LoadLoad屏障
      • Load語句1; LoadLoad屏障; Load語句2
      • 在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
    • StoreStore屏障
      • Store語句1; StoreStore屏障; Store語句2
      • 在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
    • LoadStore屏障
      • Load語句1; LoadStore屏障; Store語句2
      • 在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
    • StoreLoad屏障
      • Store語句1; StoreLoad屏障; Load語句2
      • 在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。

    volitile 的實現(xiàn)原理?

    1、Java 字節(jié)碼層面

    • ACC_VOLATILE

    2、JVM 層面

  • 規(guī)定了一系列 happens-before 原則

  • 對于 volatile 內(nèi)存區(qū)的讀寫,寫操作和讀操作前后都加了屏障

    StoreStoreBarrier
    volatile 寫操作
    StoreLoadBarrier

    LoadLoadBarrier
    volatile 讀操作
    LoadStoreBarrier

  • 3、OS 和硬件層面

    使用 volatile 變量進行寫操作,匯編指令帶有 lock 前綴,相當于一個內(nèi)存屏障,后面的指令不能重排到內(nèi)存屏障之前。

    使用 lock 前綴,引發(fā)兩件事:

    ① 將當前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存。

    ②使其他處理器的緩存無效。

    相當于對緩存變量做了一次 store 和 write 操作,讓 volatile 變量的修改對其他處理器立即可見。

    • windows lock 指令實現(xiàn)

    • MESI 緩存一致性協(xié)議實現(xiàn)

    什么是 happens-before 8條原則?

    JVM 規(guī)定,重排序必須遵守的規(guī)則——“先行發(fā)生原則”,由具體的JVM實現(xiàn)。

    對于會改變結(jié)果的重排序, JMM 要求編譯器和處理器必須禁止。

    對于不會改變結(jié)果的重排序,JMM 不做要求。

    **程序次序規(guī)則:**一個線程內(nèi)寫在前面的操作先行發(fā)生于后面的。
    管程鎖定規(guī)則: unlock 操作先行發(fā)生于后面對同一個鎖的 lock 操作。
    **volatile 規(guī)則:**對 volatile 變量的寫操作先行發(fā)生于后面的讀操作。
    **線程啟動規(guī)則:**線程的 start 方法先行發(fā)生于線程的每個動作。
    **線程終止規(guī)則:**線程中所有操作先行發(fā)生于對線程的終止檢測。
    **對象終結(jié)規(guī)則:**對象的初始化先行發(fā)生于 finalize 方法。
    **傳遞性:**如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C 。

    什么是 as-if-serial?

    不管如何重排序,單線程執(zhí)行結(jié)果不會改變,看起來像是串行的一樣。編譯器和處理器必須遵循 as-if-serial 語義。

    為了遵循 as-if-serial,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作重排序,因為這種重排序會改變執(zhí)行結(jié)果。但是如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。

    as-if-serial 保證單線程程序的執(zhí)行結(jié)果不變,happens-before 保證正確同步的多線程程序的執(zhí)行結(jié)果不變。這兩種語義的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下盡可能提高程序執(zhí)行并行度。

    JUC包下新的同步機制

    CAS

    Compare And Swap (Compare And Exchange) / 自旋 / 自旋鎖 / 無鎖 (無重量鎖)

    因為經(jīng)常配合循環(huán)操作,直到完成為止,所以泛指一類操作

    cas(v, a, b) ,變量v,期待值a, 修改值b

    CAS 存在的 ABA 問題

    ABA問題:你的女朋友在離開你的這段兒時間經(jīng)歷了別的人。自旋就是你空轉(zhuǎn)等待,一直等到她接納你為止。

    ABA 問題的解決方式:加版本號(數(shù)值型 / bool 型)

    • 基礎(chǔ)數(shù)據(jù)類型即使出現(xiàn)了ABA,一般問題不大。
    • 解決方式:加版本號(數(shù)值/bool類型)每改變一次,版本號+1;后面檢查的時候連版本號一起檢查。
    • Atomic里面有個帶版本號的類 AtomicStampedReference,目前還沒有人在面試的時候遇到過。

    Atomic 包里的類基本都是使用 Unsafe 實現(xiàn)的,Unsafe 只提供三種 CAS 方法:compareAndSwapInt、compareAndSwapLong、compareAndSwapObject,例如,原子更新 Boolean 是先轉(zhuǎn)成整形再使用 compareAndSwapInt

    AtomicInteger 原理

    incrementAndGet() 方法,不用你加鎖,也能實現(xiàn)以原子方式將當前的值加 1,它的實現(xiàn)原理:

  • 在 for 死循環(huán)中取得 AtomicInteger 里存儲的數(shù)值

  • 對 AtomicInteger 當前的值加 1

  • 調(diào)用 compareAndSet 方法進行原子更新,先檢查當前數(shù)值是否等于 expect,如果等于則說明當前值沒有被其他線程修改,則將值更新為 next,否則會更新失敗返回 false,程序會進入 for 循環(huán)重新進行 compareAndSet 操作。

    源碼級別的實現(xiàn)原理:

    • getAndIncrement()調(diào)用 Unsafe.class 類 getAndAddInt(...)

    • getAndAddInt(...) 調(diào)用 this.compareAndSwapInt(...), native 方法, hotspot cpp 實現(xiàn)

    • 這個方法在 unsafe.cpp 中

      UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj);jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 注意這里 cmpxchg UNSAFE_END
    • cmpxchg 在 atomic.cpp 中,里面調(diào)用了另外一個 cmpxchg ,到最后你會來到 atomic_linux_x86.inline.hpp , 93行 cmpxchg ,用內(nèi)聯(lián)匯編的方式實現(xiàn)。

      // atomic_linux_x86.inline.hpp inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {int mp = os::is_MP(); // is_MP = Multi Processor,如果是多處理器,則在前面加lock指令__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory");return exchange_value; }

      jdk8u: atomic_linux_x86.inline.hpp

      #define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

      最終實現(xiàn):cmpxchg ,相當于使用 CAS 的方法修改變量值,這個在 CPU 級別是有原語支持的。

      lock cmpxchg // 這個指令,在執(zhí)行這條指令的過程中,是不允許被其他線程打斷的! // 硬件層面的實現(xiàn),是鎖北橋信號,雖然也是加鎖了,但是這個鎖比操作系統(tǒng)級別、比JVM級別的鎖的效率會高跟多

    JUC 包下的一些用于同步的類

    • AtomicInteger,上面講了

    • AtomicLong

    • ReentrantLock

      • 可重入鎖
      • 必須要finally中手動釋放鎖
      • 可以指定為公平鎖
    • LongAdder

      • LongAdder內(nèi)部做了一個類似于分段鎖,最終將每一個向上遞增的結(jié)果加到一起,比 AtomicXXX 快
    • CountDownLatch

      • 門栓,每次調(diào)用 countDown 方法時計數(shù)器減 1,await 方法會阻塞當前線程直到計數(shù)器變?yōu)?
      • 和 Join 的對比:CountDownLatch可以更靈活,因為在一個線程中,CountDownLatch可以根據(jù)你的需要countDown很多次。而 join 是等待所有 join 進來的線程結(jié)束之后,才繼續(xù)執(zhí)行被 join 的線程。
    • CyclicBarrier

      • 循環(huán)柵欄:這里有一個柵欄,什么時候人滿了,就把柵欄推倒,嘩啦嘩啦的都放出去,出去之后,柵欄又重新起來,再來人,滿了推倒,以此類推。
      • 使一個線程等待其他線程各自執(zhí)行完畢后再執(zhí)行。是通過一個計數(shù)器來實現(xiàn)的,計數(shù)器的初始值是線程的數(shù)量。每當一個線程執(zhí)行完畢后,計數(shù)器的值就-1,當計數(shù)器的值為0時,表示所有線程都執(zhí)行完畢,然后在閉鎖上等待的線程就可以恢復(fù)工作了。
      • 適用于多線程計算數(shù)據(jù),最后合并計算結(jié)果的應(yīng)用場景。
    • Phaser

      • 按照不同的階段來對線程進行執(zhí)行
      • 場景:n個人全到場才能吃飯,全吃完才能離開,全離開才能打掃
    • ReadWriteLock

      • 讀寫鎖,其實就是 shared 共享鎖exclusive 排他鎖
      • 讀寫有很多種情況,比如,你數(shù)據(jù)庫里的某條數(shù)據(jù),你放在內(nèi)存里讀的時候特別多,你改的次數(shù)并不多。這時候?qū)⒆x寫的鎖分開,會大大提高效率,因為讀操作本質(zhì)上是可以允許多個線程同時進行的。
    • Semaphore

      • 信號量,類似于令牌桶,用來控制同時訪問特定資源的線程數(shù)量,通過協(xié)調(diào)各個線程以保證合理使用公共資源。信號量可以用于流量控制,特別是公共資源有限的應(yīng)用場景,比如數(shù)據(jù)庫連接。

      • 可以用于限流:最多允許多少個 線程同時在運行

      • Semaphore 的構(gòu)造方法參數(shù)接收一個 int 值,表示可用的許可數(shù)量即最大并發(fā)數(shù)。

        使用 acquire 方法獲得一個許可證,使用 release 方法歸還許可,用 tryAcquire 嘗試獲得許可

    • Exchanger

      • 可以想象 exchanger 是一個容器,用來在兩個線程之間交換變量,用于線程間協(xié)作
      • 兩個線程通過 exchange 方法交換數(shù)據(jù),第一個線程執(zhí)行 exchange 方法后會阻塞等待第二個線程執(zhí)行該方法,當兩個線程都到達同步點時這兩個線程就可以交換數(shù)據(jù),將本線程生產(chǎn)出的數(shù)據(jù)傳遞給對方。應(yīng)用場景包括遺傳算法、校對工作等。
    • LockSupport

      • 在線程中調(diào)用LockSupport.park(),阻塞當前線程
      • LockSupport.unpark(t) 喚醒 t 線程
      • unpark 方法可以先于 park 方法執(zhí)行,unpark 依然有效
      • 這兩個方法的實現(xiàn)是由 Unsafe 類提供的,原理是操作線程的一個變量在0,1之間切換,控制阻塞和喚醒
      • AQS 就是調(diào)用這兩個方法進行線程的阻塞和喚醒的。

    AQS 原理(AbstractQueuedSyncronizer,抽象的隊列式同步器)

    是一個用于構(gòu)建鎖和同步容器的框架,AQS解決了在實現(xiàn)同步容器時設(shè)計的大量細節(jié)問題。事實上,concurrent 包內(nèi)許多類都是基于 AQS 構(gòu)建的。

    如 ReentrantLock, Semaphore, CountDownLatch, CyclicBarrier 等并發(fā)類均是基于 AQS 實現(xiàn)的,具體用法:通過繼承 AQS 實現(xiàn)其模板方法,然后將子類作為同步組件的內(nèi)部類。

    ReentrantLock的基本實現(xiàn),可以概括為:

    先通過CAS嘗試獲取鎖。如果此時已經(jīng)有線程占據(jù)了鎖,那就加入AQS隊列并且被掛起。當鎖被釋放之后,排在CLH 隊列隊首的線程會被喚醒,然后CAS再次嘗試獲取鎖。在這個時候,如果:

    • 非公平鎖:如果同時還有另一個線程進來嘗試獲取,那么有可能會讓這個線程搶先獲取;

    • 公平鎖:如果同時還有另一個線程進來嘗試獲取,當它發(fā)現(xiàn)自己不是在隊首的話,就會排到隊尾,由隊首的線程獲取到鎖。

    它使用一個 volatile int state 變量作為共享資源。每當有新線程請求資源時,都會進入一個 FIFO 等待隊列,只有當持有鎖的線程釋放鎖資源后該線程才能持有資源。

    等待隊列表示 排隊等待鎖的線程,通過 雙向鏈表 實現(xiàn),線程被封裝在鏈表的 Node 節(jié)點中。隊頭節(jié)點稱作“哨兵節(jié)點”或者“啞節(jié)點”,它不與任何線程關(guān)聯(lián)。其他節(jié)點與等待線程關(guān)聯(lián),每個節(jié)點維護一個等待狀態(tài) waitStatus。

    Node 的等待狀態(tài)包括:
  • CANCELLED(線程已取消)
  • SIGNAL(線程需要喚醒)
  • CONDITION (線程正在等待)
  • PROPAGATE(后繼節(jié)點會傳播喚醒操作,只在共享模式下起作用)。
  • AQS 的底層是 CAS + volitile,用 CAS 替代了鎖整個鏈表的操作

    VarHandle 類

    Varhandle 為 java 9 新加功能,用來代替 Unsafe 供開發(fā)者使用。

    相當于引用,可以指向任何對象或者對象里的某個屬性,相當于可以直接操作二進制碼,效率上比反射高,并封裝有compareAndSet,getAndSet等方法,可以原子性地修改所指對象的值。比如對long的原子性賦值可以使用VarHandle

    • 普通屬性也可以進行原子操作
    • 比反射快,直接操作二進制碼

    總結(jié)

    以上是生活随笔為你收集整理的面试必会系列 - 1.5 Java 锁机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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