尚硅谷-JUC篇

狂神說-JUC:https://www.cnblogs.com/meditation5201314/p/14940976.html

尚硅谷-JUC:https://www.bilibili.com/video/BV1ar4y1x727?p=166&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

狂神的內容偏基礎,屬於拋磚引玉的那種

尚硅谷偏向更多原理和能力提升。(但是周陽老師對一些名詞講得好晦澀,明明可以用簡單幾句話講明白的,愣是能湊成一節課)

以後學完一門課花個腦圖,便於記憶回顧總結

csdn csdn csdn csdn csdn


	線程流程控制:
		0. 線程串行化:CompletableFutre.thenapply獲取其他線程返回值
		1. 中斷線程:循環+interrupted控制線程中斷跳出
		2. 取消線程:futuretask.cacel,或者直接拋出異常,處理
		3. 全部完成再返回:CompletableFuture的allof
		4. 線程交互:不同線程對同一個同步對象進行this.wait, this.notify(屬於Object上方法)
		5. 線程變量的感知:volatile,synchronized, atomic
		6. 線程池執行返回值:CompletableFuture放入池中即可

🔥1. 基本概念

1.1. 基本概念

1.1.1 線程Start

內部是調用C++源碼,C++底層是操作系統分配一個線程啓動

private native void start0();
1.1.2 基本名詞
1.1.2.1 一鎖/二並/三程

​ 一鎖、二並、三程

1. synchronized鎖
1. 併發和並行
3. 進程、線程、管程
 	1. 管程:就是motitor鎖,就是進入、退出一個對象或代碼收到保護,保證同步。
1.1.2.2 用戶線程/守護線程

​ 用戶線程:系統工作的線程,類似於主人

​ 守護線程:爲其他線程服務,類似於影子,用戶線程都沒了,守護線程也沒

​ main方法啓動時,就有一個主線程在執行(也是用戶線程)

1.1.3 CompletableFuture
1.1.3.1 Future接口

​ Future可以fork另一個子線程,獲取子線程結果,取消子線程執行,判斷任務是否被取消,判斷任務是否執行完畢等。

1.1.3.2 Get源碼

​ get會循環阻塞,查看某個變量,屬於在自旋,需要配合FutureTask類和run()方法一起看,當run結束後,會改變裏面的一個變量,然後get就能收到變更了。

1.1.3.3 底層源碼

​ 是實現了Future異步和CompletionStage(分多個階段觸發)

1.1.3.4 基本使用

​ 感覺周陽老師這裏講的不是很系統,推薦雷神-穀粒商城講的CompletableFuture

https://www.bilibili.com/video/BV1np4y1C7Yf?p=195&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

1.1.4 鎖
1.1.4.1 樂觀鎖/悲觀鎖

​ 樂觀鎖:默認不添加鎖,只有在更新的時候判斷一下,適合讀操作多。判斷規則:

	1. 利用version判斷。update XXX set version = version + 1 where version = X
	1. CAS算法。Atomic原子類遞增操作就是CAS自旋實現

​ 悲觀鎖:獲取數據就加鎖,防止其他線程修改,適合寫多讀少操作。synchronized,Lock都是悲觀鎖

1.1.4.2 八鎖

推薦狂神講的八鎖問題,感覺都差不多,就是鎖對象、鎖類、普通方法的區別。

https://www.cnblogs.com/meditation5201314/p/14940976.html#2八鎖現象synchronizedstatic

1.1.4.3 synchronized鎖--持有方式
1.  static synchronize Method(){}:鎖類
  	1.  由ACC_STATIC,ACC_SYNCHRONIZED判斷,然後進去monitirEnter,exit邏輯
2.  synchronized  Method(){}:鎖對象
  	1.  先由ACC_SYNCHRONIZED判斷,然後再進入monitorEnter,monitorExit邏輯
3.  synchronized(this.Obj){}:同步代碼塊
  	1.  底層就是monitorEnter和monitoerexit構成
1.1.4.4 synchronized鎖--底層原理

​ 一個對象Object創建就附帶了objectMonitor對象監視器,用來記錄哪個線程哪個鎖、鎖了幾次等相關信息。synchronized鎖對象、鎖類的時候是管程monitor就變更了對象ObjectMonitor信息。

1.1.4.5 公平/非公平鎖

​ 公平鎖:儘量按照順序來申請(會有線程切換的開銷)

​ 非公平:讓線程爭搶

1.1.4.6 可重入鎖

​ 一個線程內部多個流程可重複獲取同一把鎖,不會產生死鎖。synchronized(隱式可重入鎖,內部有objectMonitor鎖計數器進行加一減一),reentreenLock(顯示可重入鎖,lock一次,就必須unlock一次)都是可重入鎖。

1.1.4.7 Synchronized鎖--執行原理
/*
	1. synchronized的字節碼指令monitorEnter對對象裏面的objectMonitor字段修改。判斷是鎖對象是否被調用,加鎖幾次,然後進行加鎖
	2. 加完鎖後處理正常業務,其他線程獲取鎖對象時失敗會進去阻塞隊列
	3. 執行完後monitorExit進行數據-1,解鎖
*/
synchronized(this){
    
}
1.1.5 線程中斷
1.1.5.1 實現方式

線程中斷應有由線程自己決定中斷

  1. thread自帶中斷api實現
  2. volatile 全局變量,對各個線程可見
  3. atomicBoolean 全局變量,對各個線程可見
1.1.5.2 源碼分析
  1. 正常活動線程:interrupted會標誌爲中斷,中斷後並不會結束運行,過一段時間後才能結束。中斷已經結束的線程,不會產生任何影響,isTreeupted還是false
  2. 阻塞狀態線程(sleep,wait.join):當其他線程或本線程調用該線程的interrupted,中斷狀態將清除,變成false,並報錯interruptedException

用戶查看線程是否中斷

1.1.5.3 靜態方法VS實例方法

​ Thread.interrupted():靜態方法,返回線程中斷狀態並置爲false

​ Thread.currentThread().isInterrupted():實例方法,返回線程中斷狀態

​ Thread.currentThread().interrupt():中斷某個線程。不會讓線程結束,可以配合循環使用

1.1.5 LockSupport
1.1.5.1 等待喚醒--實現方式(生產者-消費者)

​ 用來優化之前的線程喚醒和阻塞,比其他喚醒方法功能更強(下面3個都只能有一個,多次的話會失效)

1. Object->wait(), notify, notifyAll
 	1. wait,notify必須放在同步對象裏面
 	2. wait,notify先後順序不能錯,否則先喚醒後wait無法喚醒
2. Condition-> await(), signal()
 	1. await,signal必須放在同一個lock,unlock裏面
 	2. await,signal先後順序也不能亂
3. LockSupport-> park阻塞、unpark喚醒線程
 	1. park阻塞線程,unpark(線程)。就類似給了一個許可證,以後永久可用。先後順序可以不一致,都能喚醒。許可證只有一個,即一個park,一個unpark
1.1.6 Java內存模型--JMM
1.1.6.1 基本概念

​ Java Memory Mode:用來實現操作系統和硬件之間內存訪問的差異,實現在各個平臺內存訪問速度一致的效果。

​ 原子性:操作不可打斷

​ 可見性:多個線程對主內容中數據進行拷貝一份變量副本,對主內存中共享變量的修改對其他線程可見。(每個線程自己有工作內存存變量副本,不同線程之間不可互相訪問)

​ 有序性:指令會重排序,但執行結果一定一致

1.1.6.2 JMM規範--先行發生原則

正是因爲有了先行發生原則,所以實際代碼才少了很多volatile, synchronized定義

  1. 線程A操作一個變量後對變量變更需要B操作可見,
  2. A操作裏面的各種代碼子操作順序可以隨着編譯器優化而有所不同,只要最終結果不變就行
  3. 順序性:代碼先寫的比代碼後寫的快、對象創建後才能finalize、lock後才能unlock,start()先於線程操作前、線程修改變量後對後續線程獲取變量可見(不安全的情況就是普通變量兩個線程同時獲取在修改)

在這裏插入圖片描述

1.2 代碼名詞

1.2.1 Volatile
1.2.1.1 基本特點
  1. 可見性:修改某個變量對其他線程都要可見

  2. 有序性:禁止指令重排

  3. 沒有原子性:volatile i++不能保證原子性

1.2.1.2 內存屏障
1. 對volatile寫操作都立刻刷到主內存中
1. 對volatile讀操作都先把線程內部的內存變量定義無效,然後從主內存中讀取
1. 在讀寫操作前後加入了組織指令重排的約束

​ 注:感覺老師這裏對內存交互講的不是很好,還是狂神講的簡單易懂,學習鏈接:https://www.bilibili.com/video/BV1B7411L7tE?p=30&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

1.2.1.3 存在問題

​ DCL:doucle check lock雙端檢測鎖:

//如果不加volatile,那麼當new對象的時候,由於內部是先創建對象位置,然後指令重排,然後user指向空對象,然後在給對象放入對象位置。當user指向空對象的時候,多線程下第一次if判斷就會發生問題
volatile User user = null;

if (user ==null){
    synzhronized(UserTest.class){
        if(user == null){
            user = new User;
        }
    }
}
1.2.2 CAS
1.2.2.1 基本概念

​ Compare and swap:內部原理是樂觀鎖,如果共享內存值不一樣,就放棄,或者自旋重試。

1.2.2.2 Atomic自增--線程安全

通過Unsafe類(內部都是native方法)操作硬件保證非阻塞的原子性操作,屬於CPU指令原語。

​ 內部AtomicInteger是利用CAS自旋 + volatile + native方法來保證原子性

--unsafe類中方法
    public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //重新獲取volatile變量
        var5 = this.getIntVolatile(var1, var2);
        //比較與預期值,不同就自旋
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
1.2.2.3 存在問題
1. 自旋開銷大
1. ABA問題: compareStampReference解決(可以包裝基本類型和對象)
1.2.3 原子類
1.2.3.1 基本分類
1. 基本類型、數組類型
1. 引用類型:AtomicReference(普通對象包裝) AtomicStampedReference(版本標記), atomicMarkableReference(一次性標記)
1. 線程安全--類屬性修改:AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater
1.2.3.2 LongAdder--基本概念

​ 內置一個base變量和cell[]數組

​ Atomic:底層是原語和自旋,一次只允許一個線程操作,高併發下性能下降

​ LongAdder:就是採用cell[]數組存,然後獲取的時候把base變量和cell[]數組值加起來

1.2.3.3 LongAdder--源碼分析(略)

​ 總的來說,LongAdder可用,get併發下獲取數據性能好,但是sum精度不準確

1.2.4 ThreadLocal
1.2.4.1 基本概念

​ 給每個線程一個變量副本。對應JMM中的線程變量副本。

之前synchronized,volatile, cas都是對共享變量的操作,現在ThreadLocal是對線程私有變量的操作

1.2.4.2 基本使用
// 1.初始化
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

// 2. 使用後remove

// 3.線程池開啓後要shutdown,否則會一直開啓

// 4.使用static修飾,每次new對象都只會有一份
1.2.4.3 底層原理

​ Thread 內置ThreadLocal,ThreadLocal內置ThreadLocalMap。

​ 當賦值的時候,不存在就創建(和hashmap類似),賦值就是給map<threadLocal實例,值>這麼賦值

1.2.4.4 強/軟/弱/虛引用

這幾個引用, finalize都是Object下面的,

  1. 強引用:普通的引用對象
  2. 軟引用:內存不足會回收
  3. 弱引用:GC發現就會回收
  4. 虛引用:必須被回收,get是null,就是對象回收後的一種通知機制,配合引用隊列使用,和finallize類似
1.2.4.5 key-弱引用
1. 回收key: GC發現線程死亡了,threadLocal的key就被回收,避免內存泄漏(類似人和身份證)
1. 回收value: 當GC回收了threadLocal的key後,任意每次調用ThreadLocal的get,set,remove都會回收key=null的value值
1. 所以全靠調用get,set,remove也會存在內存泄漏,每次用完都要remove
1.2.5 內存佈局

​ 一個對象object在內存中存下來分爲

  1. 對象頭
    1. 運行時元數據(markword):記錄了鎖的狀態(無鎖、偏向、輕量、重量)
    2. 類型指針
  2. 實例數據
  3. 填充數據
1.2.6 鎖升級
1.2.6.1 基本概念

​ 鎖升級:無鎖-> 偏向鎖->輕量->重量。 (寫鎖叫獨佔鎖,讀鎖叫共享鎖)

​ 對於每一個對象object,內部都有monitor,當線程用synchronized修飾成重量級鎖時,線程需要用戶態到內核態的切換,非常耗時。(線程阻塞、喚醒也會有用戶態、內核態的切換)

img

1.2.6.2 鎖升級
  1. 無鎖(標記位001):對象默認無鎖

  2. 偏向鎖(默認開啓 標記位101 需要項目啓動後等一會對象創建就是偏向鎖了):對象的對象頭中markWord存儲的是偏向線程id。

    1. 概念:單線程競爭下獲取鎖時,同一個線程頻繁獲取一個鎖,後續會自動獲得該鎖(類似熟人一直來某個地方)
    2. 實現:對象頭記錄了對象是否被某個線程搶到,搶到了就標記線程id,後續就能標記該線程和其他線程,若其他線程來競爭成功,就利用CAS替換新線程id,若其他線程競爭失敗,就需要進行鎖升級
      1. 對於鎖升級還是競爭成功替換線程id,這是由原線程是否執行完synchronized,如果執行完同步代碼塊,就能替換,剛進入,就要鎖升級)
  3. 輕量級鎖:markWord存線程棧中Lock Record指針

    1. 概念:2個(多個)線程交替獲取對象的鎖
    2. 實現:A線程獲得,B線程競爭,競爭成功,對象偏向鎖改爲B,B競爭失敗,對象偏向鎖升級爲輕量級鎖,B自旋等待獲取A持有的輕量級鎖。(之所以存在線程棧中,就是把對象markword複製到線程棧幀中了,釋放輕量級鎖也是把棧幀中寫回到markword中)。B線程如果自旋到一定次數(自適應次數,因爲更多線程來了)後會升級成重量級鎖
  4. 重量級鎖:存堆中monitor指針

    1. 概念:synchronized重量級鎖,基於Monitor存儲線程id來保證其他線程無法獲取
輕量級鎖和偏向鎖

​	輕量級鎖競爭失敗就會自旋競爭,偏向只有一個線程

​	輕量級鎖每次退出同步代碼塊就要釋放鎖,而偏向鎖只有多線程競爭纔會釋放鎖,因爲默認是一個線程

img

1.2.6.3 鎖--HashCode存儲

​ 無鎖有Hashcode,計算了之後就無法進入偏向鎖,進行鎖升級成輕量級鎖

​ 處於偏向鎖還要計算HashCode,就會變成重量級鎖獲取Hashcode

​ 輕量、重量鎖都有HashCode

1.2.6.4 Synchronized鎖總結

​ 1. Synchronized本質就是先自旋,再阻塞,內部實現用對象頭的標記實現無鎖、偏向、輕量、重量的升級。(改變了jdk1.6之前都弄成重量級鎖的壓力)

1.2.6.5 鎖消除/鎖粗化

​ 鎖消除:多線程獲取同步代碼塊的時候,裏面每次重新new對象,synchronized(對象)就會有JIT優化成直接new對象。對應JVM裏面JIT逃逸分析-同步省略

​ 鎖粗化:多個synchronized包同一個對象,就會優化成一個synchronized包這個對象

        //鎖粗化
		Object o = new Object();
        //優化成synchronized(o){sout(1), sout(2)}
        synchronized (o){
            System.out.println("1");
        }
        synchronized (o){
            System.out.println("2");
        }

​ 無鎖:自由自在

​ 偏向:唯我獨尊

​ 輕量:楚漢爭霸

​ 重量:羣雄逐鹿


1.2.7 AQS
1.2.7.1 基本概念

​ AbstractQueuedSynchronizer:抽象隊列同步器

1. 底層是有一個FIFO的CLH等待隊列(類似候客廳)和int型state來標記資源是否被獲得
2. 由AQS實現的上層:reentranlock, countdownlatch, semaphore
1.2.7.2 內部實現

AQS = state + CLH隊列

Node = waitStatud + Thread

  1. CLH是由一個Node結點來實現隊列
    1. Node結點內部有標記線程是共享/獨享狀態,等待狀態
  2. 有個頭尾指針指向上面Node結點
  3. volatile int state由於標記資源狀態
    class Node{}
	private transient volatile Node head;

    private transient volatile Node tail;
    private volatile int state;

image-20220722150943139

Node結點內部屬性說明

image-20220722162130795

1.2.7.3 源碼剖析

ReentranLock源碼

  1. 非公平就先搶佔state獨佔,然後調用nonfairTryAcquire到CLH隊列,後續循環
  2. 公平一來就調用公平鎖的隊列,先進先出

非公平鎖和公平鎖內部acquire方法

1. tryAcquire(): 公平鎖有個排隊函數,其他都和非公平鎖一樣,嘗試CAS自旋獲取state狀態
1. acquireQueued,addwaiter: addwaiter根據當前線程創建結點,acquireQueued:  根據上一個Node結點中volatile waitStatus判斷線程是否需要LockSupport.park()暫停,就類似於阻塞在等待隊列中

		3. unlock解鎖:
     			1. 把類似於辦理好業務的線程窗口置爲null, state置爲0,waitStatus修改,並喚醒unpark正在等待的線程
     			2. unpark的線程要調用各自的tryAcquire(),用於區分公平鎖和非公平鎖

image-20220723105812016

1.2.8 讀寫鎖
1.2.8.1 基本概念

​ 讀寫鎖:適合讀多寫少的情景,運行同時多個讀,但是讀寫,寫寫互斥。底層也是AQS的Sync

演變流程就是從無鎖->獨佔鎖->讀寫鎖->郵戳鎖

image-20220723160240915

1.2.8.2 鎖降級

​ readWriteLock鎖降級:由寫鎖降級爲讀鎖(類似linux中寫權限比讀權限高)。就是先加寫鎖,在加讀鎖,再釋放寫鎖,再釋放讀鎖。

不同線程操作同一對象時讀寫是互斥的,但是同一個線程內容是可以鎖降級。

​ 作用:當程序中想實現寫後讀的話,就用到這種鎖降級方法,否則寫了釋放寫鎖再獲取讀鎖就可能被其他寫線程給獲取到修改了。

1.2.8.3 StampedLock郵戳鎖

​ 之前讀鎖未釋放前都不允許插入寫鎖,所以可能存在多個讀鎖,一個寫鎖情況下,發生寫鎖無法插入,這就是鎖飢餓。所以爲了保證讀沒有完成的時候寫鎖可以介入,從而引入了stampedLock

​ 實現方式:

1. 讀模式:和之前readwritelock一樣
1. 寫模式:和之前readwritelock一樣
1. 樂觀模式:就是在讀的時候用樂觀鎖判斷,如果發現被改過,就悲觀讀

1.3 個人總結--腦圖

image-20220731123106243


	以前一直想不通爲什麼線程要區分這麼細,後來才發現,實際開發中,不同用戶就是一個線程請求,如何把各個請求處理好,保證系統性能和安全性,纔是程序員需要考慮的問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章