多線程JUC併發篇常見面試詳解

来源:https://www.cnblogs.com/zbqblogs/archive/2022/04/16/16154384.html
-Advertisement-
Play Games

樹 樹的定義 樹是一種數據結構,樹結構只有一個根節點,除根節點外,其餘節點被分成M(M>0) 個互不相交的集合T1,T2,T3,......,Tm. 其中每一個集合Ti(1 < i < m)又是一顆與樹結構類似的子樹。每個子樹的根節點有且只有一個前驅,可以有0個或多個後繼。因此,樹是遞歸定義的。 如 ...


@

目錄

多線程JUC併發篇

1、JUC 簡介

什麼是 JUC ?

  • JUC 就是 java.util.concurrent 下麵的類包,專門用於多線程的開發
    在這裡插入圖片描述
    為什麼使用 JUC ?

  • 以往我們所學,普通的線程代碼,都是用的thread或者runnable介面

  • 但是相比於callable來說,thread沒有返回值,且效率沒有callable高

2、線程和進程

  • 進程就是一個應用程式

  • 線程是進程中的一個實體,線程本身是不會獨立存在的。

進程是代碼在數據集合上的一次運行活動, 是系統進行資源分配和調度的基本單位。

線程則是進程的一個執行路徑, 一個進程中至少有一個線程,進程中的多個線程共用進程的資源。

​ 操作系統在分配資源時是把資源分配給進程的, 但是CPU 資源比較特殊, 它是被分配到線程的, 因為真正要占用CPU 運行的是線程, 所以也說線程是CPU 分配的基本單位。

​ java預設有幾個線程? 兩個 main線程 gc線程

​ Java 中,使用 Thread、Runnable、Callable 開啟線程。

​ Java 沒有許可權開啟線程 、Thread.start() 方法調用了一個 native 方法 start0(),它調用了底層 C++ 代碼。

3、並非與並行

併發多線程操作同一個資源,交替執行

  • CPU一核, 模擬出來多條線程,天下武功,唯快不破,快速交替

並行(多個人一起行走, 同時進行)

  • CPU多核,多個線程同時進行 ; 使用線程池操作

4、線程的狀態

  • 新建

  • 就緒

  • 阻塞

  • 運行

  • 死亡

5、wait/sleep的區別

  • 來自不同的類:wait來自object類, sleep來自線程類

  • 關於鎖的釋放:wait會釋放鎖, sleep不會釋放鎖

  • 使用範圍不同:wait必須在同步代碼塊中,sleep可以在任何地方睡

  • 是否需要捕獲異常:wait不需要捕獲異常,sleep需要捕獲異常

6、Lock 鎖(重點)

Synchronized 傳統的鎖

之前我們所學的使用線程的傳統思路是:

  • 單獨創建一個線程類,繼承Thread或者實現Runnable

  • 在這個線程類中,重寫run方法,同時添加相應的業務邏輯

  • 在主線程所在方法中new上面的線程對象,調用start方法啟動

1、Lock鎖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aFbI65Pz-1650013582522)(https://pizximfzuc.feishu.cn/space/api/box/stream/download/asynccode/?code=Nzg2NDU1MDkyYTk1YWVlZDJjMzM3M2QxODNlMWM4NWRfeUZORjNjcWtmd3ZrR1FmWEZ2MkVWdjZMWGtHenpsM3JfVG9rZW46Ym94Y25qVzdjWERnR2owZVlMRXU4S3pTT1VjXzE2NTAwMTM0OTI6MTY1MDAxNzA5Ml9WNA)]
可以看到,

Lock是一個介面,有三個實現類,現在我們使用

ReentrantLock 就夠用了

查看

ReentrantLock 源碼,構造器

2、公平非公平:

  • 公平鎖::十分公平, 可以先來後到,一定要排隊

  • 非公平鎖::十分不公平,可以插隊(預設)

3、ReentrantLock 構造器

  • ReentrantLock 預設的構造方法是非公平鎖(可以插隊)。

  • 如果在構造方法中傳入 true 則構造公平鎖(不可以插隊,先來後到)。

4、Lock 鎖實現步驟:

  1. 創建鎖,new ReentrantLock()
  2. 加鎖,lock.lock()
  3. 解鎖,lock.unlock()
  4. 基本結構固定,中間的業務自己靈活修改

7、synchronized 和 lock 鎖的區別

  1. synchronized 是內置的 Java 關鍵字,Lock 是一個 Java 類

  2. synchronized 無法判斷獲取鎖的狀態,Lock可以判斷是否獲取到了鎖

  3. synchronized 會自動釋放鎖,Lock 必須要手動釋放鎖!如果不釋放鎖,會產生死鎖

  4. synchronized 假設線程1(獲得鎖,然後發生阻塞),線程2(一直等待); Lock 鎖就不一定會等待下去,可使用 tryLock 嘗試獲取鎖

  5. synchronized 可重入鎖,不可以中斷的,非公平的;Lock鎖,可重入的,可以判斷鎖,是否公平(可自己設置)

  6. synchronized 適合鎖少量的代碼同步問題,Lock 適合鎖大量的同步代碼

總體來說,synchronized 本來就是一個關鍵字,很多規則都是定死的,靈活性差;Lock 是一個類,靈活性高

8、生產者和消費者問題(通信問題)

1、Synchronized 版本

解決線程之間的通信問題,比如線程操作一個公共的資源類

基本流程可以總結為:

  • 等待:判斷是否需要等待

  • 業務:執行相應的業務

  • 通知:執行完業務通知其他線程

public class ConsumeAndProduct {
    public static void main(String[] args) {
        Data data = new Data();
        // 創建一個生產者
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        // 創建一個消費者
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}
//這是一個緩衝類,生產和消費之間的倉庫,公共資源類
class Data{
    // 這是倉庫的資源,生產者生產資源,消費者消費資源
    private int num = 0;
    // +1,利用關鍵字加鎖
    public synchronized void increment() throws InterruptedException {
        // 首先查看倉庫中的資源(num),如果資源不為0,就利用 wait 方法等待消費,釋放鎖
        if(num!=0){
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"=>"+num);
        // 通知其他線程 +1 執行完畢
        this.notifyAll();
    }
    // -1
    public synchronized void decrement() throws InterruptedException {
        // 首先查看倉庫中的資源(num),如果資源為0,就利用 wait 方法等待生產,釋放鎖
        if(num==0){
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"=>"+num);
        // 通知其他線程 -1 執行完畢
        this.notifyAll();
    }
}

思考問題:如果存在ABCD4個線程是否安全?

  • 不安全,會有虛假喚醒

查看 api 文檔
在這裡插入圖片描述

解決辦法:if 判斷改為 while,防止虛假喚醒

  • 因為 if 只會執行一次,執行完會接著向下執行 if() 外邊的代碼

  • 而 while 不會,直到條件滿足才會向下執行 while() 外邊的代碼

修改代碼為:

    // ...
    // 使用 if 存在虛假喚醒
    while (num!=0){
        this.wait();
    }
    // ...
    while(num==0){
        this.wait();
    }

2、JUC 版本

鎖、等待、喚醒 都進行了更換
在這裡插入圖片描述

改造之後,確實可以實現01切換,但是ABCD是無序的,不滿足我們的要求,

Condition 的優勢在於,精準的通知和喚醒線程!比如,指定通知下一個進行順序。

重新舉個例子,

三個線程 A執行完調用B,B執行完調用C,C執行完調用A,分別用不同的監視器,執行完業務後指定喚醒哪一個監視器,實現線程的順序執行

鎖是統一的,但監視器是分別指定的,分別喚醒,signal,之前使用的是 signalAll

  private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int num = 1; // 1A 2B 3C
    public void printA(){
        lock.lock();
        try {
            while (num != 1){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im A ");
            num = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            while (num != 2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im B ");
            num = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            while (num != 3) {
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im C ");
            num = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

9、八個有關鎖的問題

深入理解鎖

關於鎖的八個問題

問題1:兩個同步方法,先執行發簡訊還是打電話?

經過測試,一直是先發簡訊

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rsXbJVS7-1650013582524)(https://pizximfzuc.feishu.cn/space/api/box/stream/download/asynccode/?code=NzExNGI3OTliYmQ5MTk5ZmVjMjYzNzAxM2MwYmQwMGJfM3l3b1NKWndKYVA5cjRrVHc4SWdxektzOGQxSmZKeHVfVG9rZW46Ym94Y25nTkhuUVdVdllwVWhiYVZXS09Oak0xXzE2NTAwMTM0OTI6MTY1MDAxNzA5Ml9WNA)]

問題2:如果發簡訊延遲2秒,誰先執行

結果依舊是先發簡訊,後打電話

分析:

  • 並不是由於發簡訊在前導致的

  • 本案例中,方法前加synchronized,鎖的其實該方法的調用者,也就是 phone 實例,兩個方法共用同一個 phone 對象的鎖,誰先拿到,誰先執行

  • 在主線程中,先調用發簡訊,所以先執行,打電話等釋放鎖再執行

問題3 加上一個沒有鎖的普通方法,誰先執行

觀察發現,先執行了 hello

分析原因:

  • hello 是一個普通方法,不受 synchronized 鎖的影響,不用等待鎖釋放。

問題4:兩個對象,一個調用發簡訊,一個調用打電話,誰先執行

結論,先打電話,後發簡訊

分析原因:

  • 兩個對象兩把鎖,互不影響,1拿到鎖還需要等待3秒,2拿到對象立刻就能打電

問題5:原來的兩個同步方法,變為靜態同步方法,一個對象調用,誰先執行

結果,始終是先發簡訊,後打電話

分析原因:

靜態方法前面加鎖,鎖的其實是這個方法所在的Class類對象(非靜態那個是實例對象,註意區分)

Class類對象也是全局唯一,使用的是通一把鎖,所以先發簡訊,後打電話

雖然和上面的實例對象都是對應了全局唯一的鎖,但原理還是有所不同

主線程先執行了發簡訊,打電話就必須等鎖釋放再執行

問題6:創建兩個實例,調用兩個靜態同步方法,誰先執行

結果,現發簡訊,後打電話

原因分析:

  • 雖然實例對象是兩個,但是兩個靜態同步方法對應的鎖是Class類對象的鎖,還是全局唯一

問題7:一個靜態同步方法、一個同步方法、一個對象調用,誰先執行

結果:先打電話,後發簡訊

原因分析:

  • 靜態同步方法和普通同步方法分別對應了不同的鎖,互不幹擾

  • 發簡訊需要延遲3秒,所以打電話先執行了

問題8:兩個對象,一個調用靜態同步方法,一個調用普通同步方法,誰先執行

結果,先打電話,後發簡訊

分析原因:

同問題7相同,兩個方法對應了不同的鎖,互不幹擾

發簡訊還需要等待3秒,所以打電話先執行完了

小結

無外乎兩種鎖,一個是new實例的鎖,一個是Class對象的鎖

實例的鎖,與當前的實例唯一對應,Class對象的鎖與這個類唯一對應

如果兩個方法等同一個鎖,必須一個先執行完,釋放鎖,另一個才可以執行

如果兩個方法等不同的鎖,互不影響,誰先誰後看具體情況

在主線程中,代碼是順序執行的,再結合鎖的原理,綜合判斷線程執行的順序

10、集合類的安全問題

在 JUC 併發編程情況下,適用於單線程的集合類將出現併發問題

1、List 不安全

運行出現併發修改異常,

java.util.ConcurrentModificationException

解決方案:

解決方案1:

  • ArrayList 換成 Vector,Vector 方法裡加了鎖

  • Vector出現比較早,由於鎖導致方法執行效率太低,不推薦使用

解決方案2:

  • 使用 Collection 靜態方法,返回一個帶鎖的 List 實例

List list = Collections.synchronizedList(new ArrayList<>());

解決方案3:

  • 使用 JUC 提供的適合併發使用的 CopyOnWriteArrayList

List list = new CopyOnWriteArrayList<>();

分析:

CopyOnWrite 表示寫入時複製,簡稱COW,電腦程式設計領域的一種優化策略

多線程調用list時,讀取時沒有問題,寫入的時候會複製一份,避免在寫入時被覆蓋

這也是一種讀寫分離的思想

CopyOnWriteArrayList 比 Vector 強在哪裡?前者是寫入、複製,且使用 lock 鎖,效率比 Vector 的synchronized 鎖要高很多

2、Set 不安全

Set 和 List 同理可得:多線程情況下,普通的 Set 集合是線程不安全的

  • 使用 Collection 工具類的 synchronized 包裝的 Set 類

Set set = Collections.synchronizedSet(new HashSet<>());

  • 使用 JUC 提供的 CopyOnWriteArraySet 寫入複製

Set set = new CopyOnWriteArraySet<>();

思考,HashSet 底層到底是什麼?

  • hashSet底層就是一個HashMap;hashSet只使用了hashMap的key

11、Callable(簡單)

得到的信息

可以有返回值

可以拋出異常

方法不同,run() => call()

使用時註意

  • Callable 的泛型也是 call 方法的返回值類型

  • Callable 的實現類無法直接放在 Thread 中,還需要先放在 -

  • FutureTask 中,再放在 Thread 中FutureTask 就相當於適配類,起到牽線的作用

註意:

  • 運行結果會產生緩存,目的是為了提高效率

  • get方法可能會產生阻塞,所以放在了最後

12、JUC 常用輔助類

1、CountDownLatch

減法計數器

原理:

countDownLatch.countDown(); //數量減1

countDownLatch.await();// 等待計數器歸零,然後再向下執行

每次有線程調用countDown()數量-1,假設計數器變為0,countDownLatch.await();就會被喚醒,繼續執行

2、CyclickBarrier

加法計數器,與 CountDownLatch 正好相反

相當於設定一個目標,線程數達到目標值之後才會執行

3、Semaphore

計數信號量,比如說,有6輛車,3個停車位,汽車需要輪流等待車位

常用在需要限流的場景中,

原理:

  • *semaphore.acquire() 獲得資源,如果資源已經使用完了,就等待資源釋放後再進行使用!

  • *semaphore.release() 釋放,會將當前的信號量釋放+1,然後喚醒等待的線程!

用途:

  • *多個共用資源互斥的使用!

  • *併發限流,控制最大的線程數!

13、ReadWriteLock 讀寫鎖

ReadWriteLock,這是一個更加細粒度的鎖

// 自定義緩存
class MyCache{
    private volatile Map<String,String> map = new HashMap<>();
    private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
    // 存,寫,寫入的時候只希望只有一個線程在寫
    public void write(String key, String value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "線程開始寫入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "線程開始寫入ok");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
    // 取,讀,所有線程都可以讀
    public void read(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "線程開始讀取");
            map.get(key);
            System.out.println(Thread.currentThread().getName() + "線程讀取ok");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

小結:

  • 讀-讀 可以共存
  • 讀-寫 不能共存
  • 寫-寫 不能共存

也可以這樣稱呼,含義都是一樣,名字不同而已

  • 獨占鎖(寫鎖)一次只能由一個線程占有
  • 享鎖(讀鎖)一次可以有多個線程占有

14、阻塞隊列

1、Blockqueue

阻塞隊列 BlockQueue 是Collection 的一個子類

應用場景:多線程併發處理、線程池

BlockingQueue 有四組 API

方式 拋出異常 不會拋出異常,有返回值 阻塞等待 超時等待

添加操作 add() offer() 供應 put() offer(obj,int,timeunit.status)可設置時間

移除操作 remove() poll() 獲得 take() poll(int,timeunit.status)可設置時間

判斷隊列首部 element() peek() 偷看,偷窺 SynchronizedQueue 同步隊列

同步隊列沒有容量,進去一個元素,必須等待取出來之後,才能再往裡面放一個元素

2、SynchronizedQueue

  • SynchronizedQueue使用 put 方法和 take 方法
  • Synchronized 和 其他的 BlockingQueue 不一樣 它不存儲元素;
  • put了一個元素,就必須從裡面先 take 出來,否則不能再 put 進去值!
  • 並且 SynchronousQueue 的 take 是使用了 lock 鎖保證線程安全的。

15、線程池(重點)

池化技術

線程池重點:三大方式、七大參數、四種拒絕策略

程式的運行的本質:占用系統的資源 ! 優化CPU資源的使用 ===>池化技術(線程池、連接池、記憶體池、對象池…)

池化技術:實現準備好一些資源,有人要用,就來我這裡拿,用完之後還給我

線程池的好處:

  • 降低資源消耗
  • 提高響應速度
  • 方便管理

如何優化:

  • 線程復用,可以控制最大併發數,管理線程

1、線程池:三大方法

查看阿裡巴巴開發手冊
在這裡插入圖片描述

  1. ExecutorService threadPool = Executors.newSingleThreadExecutor();//單個線程
  2. ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //創建一個固定的線程池的大小
  3. ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸縮的(不會出現OOM)

之前我們所學知識,直接創建線程,現在我們通過線程池來創建線程,使用池化技術

>  ExecutorService service = Executors.newCachedThreadPool();//可伸縮的,遇強則強,遇弱則弱
>         try {
>             for (int i = 0; i < 10; i++) {
>                 service.execute(() -> {
>                     System.out.println(Thread.currentThread().getName() + "ok");
>                 });
>             }
>             //線程池用完要關閉線程池
>         } finally {
>             service.shutdown();
>         }

2、線程池:七大參數

public ThreadPoolExecutor(int corePoolSize,//核心線程數 也就是一直工作的線程數量
                          int maximumPoolSize,//最大線程數,如果核心心線程數使用完
                          long keepAliveTime,//非核心線程的存活時間
                          TimeUnit unit,//非核心線程的存活時間單位
                          BlockingQueue<Runnable> workQueue,//阻塞隊列
                          ThreadFactory threadFactory,//線程工廠
                          RejectedExecutionHandler handler) //拒絕策略

提交優先順序

execute()提交方法中源碼中的幾個if裡面都會調用執行方法addWorker(Rannale firstTask,boolean core )
在這裡插入圖片描述

執行優先順序
在這裡插入圖片描述
在這裡插入圖片描述

執行優先順序:

addWorker(Rannale firstTask,boolean core )

submit()與execute()區別

1、submit()有返回值,execute()沒有返回值

2、submit()方法裡面調用了execute()方法

3、四大拒絕策瑜:

在這裡插入圖片描述

16、為什麼要使用線程池?

為了減少創建和銷毀線程的次數,讓每個線程可以多次使用,可根據系統情況調整執行的線程數量,防止消耗過多記憶體,所以我們可以使用線程池.

17、線程池線程復用的原理是什麼?

首先線程池內的線程都被包裝成了一個個的java.util.concurrent.ThreadPoolExecutor.Worker,然後這個worker會馬不停蹄的執行任務,執行完任務之後就會在while迴圈中去取任務,取到任務就繼續執行,取不到任務就跳出while迴圈(這個時候worker就不能再執行任務了)執行 processWorkerExit方法,這個方法呢就是做清場處理,將當前woker線程從線程池中移除,並且判斷是否是異常的進入processWorkerExit方法,如果是非異常情況,就對當前線程池狀態(RUNNING,shutdown)和當前工作線程數和當前任務數做判斷,是否要加入一個新的線程去完成最後的任務(防止沒有線程去做剩下的任務).

那麼什麼時候會退出while迴圈呢?取不到任務的時候(getTask() == null)

/java/util/concurrent/ThreadPoolExecutor.java:1127
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {...執行任務...}
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }




private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            //(rs == SHUTDOWN && workQueue.isEmpty()) || rs >=STOP
            //若線程池狀態是SHUTDOWN 並且 任務隊列為空,意味著已經不需要工作線程執行任務了,線程池即將關閉
            //若線程池的狀態是 STOP TIDYING TERMINATED,則意味著線程池已經停止處理任何任務了,不在需要線程
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                    //把此工作線程從線程池中刪除
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            //allowCoreThreadTimeOut:當沒有任務的時候,核心線程數也會被剔除,預設參數是false,官方推薦在創建線程池並且還未使用的時候,設置此值
            //如果當前工作線程數 大於 核心線程數,timed為true
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
                        
            //(wc > maximumPoolSize || (timed && timedOut)):當工作線程超過最大線程數,或者 允許超時並且超時過一次了
            //(wc > 1 || workQueue.isEmpty()):工作線程數至少為1個 或者 沒有任務了
            //總的來說判斷當前工作線程還有沒有必要等著拿任務去執行
            //wc > maximumPoolSize && wc>1 : 就是判斷當前工作線程是否超過最大值
            //或者 wc > maximumPoolSize && workQueue.isEmpty():工作線程超過最大,基本上不會走到這,
            //                如果走到這,則意味著wc=1 ,只有1個工作線程了,如果此時任務隊列是空的,則把最後的線程刪除
            //或者(timed && timedOut) && wc>1:如果允許超時並且超時過一次,並且至少有1個線程,則刪除線程
            //或者 (timed && timedOut) && workQueue.isEmpty():如果允許超時並且超時過一次,並且此時工作                                        隊列為空,那麼妥妥可以把最後一個線程(因為上面的wc>1不滿足,則可以得出來wc=1)刪除
            if ((wc > maximumPoolSize  || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                        //如果減去工作線程數成功,則返回null出去,也就是說 讓工作線程停止while輪訓,進行收尾
                    return null;
                continue;
            }

            try {
                    //判斷是否要阻塞獲取任務
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

18、AQS的理解

1、ReentrantLock和AQS的關係

java併發包下的很多API都是基於AQS來實現加鎖和釋放等功能,AQS是並java發包的基礎類。

舉個列子,ReentrantLock、ReentrantReadWr

iteLock底層都是基於AQS來實現的。ReentrantLock內部包含已個AQS對象。

AQS的全稱是AbstractQueueSynchonizer,抽象隊列同步鎖。

2、ReentrantLock加鎖和釋放鎖的底層原理

如果現在有一個線程過來嘗試用ReentrantLock的lock()方法進行加鎖,會發生什麼?

很簡單,這個AQS對象內部有一個核心的變數state,是int類型的,代表加鎖的狀態。初始情況下為0.

另外AQS內部還有一個關鍵變數,用來記錄加鎖線程是哪個線程,初始化狀態下,這個線程是null。

[MISSING IMAGE: image-20220324164210432, image-20220324164210432 ]

接著線程1跑過來會調用ReentrantLock的lock()方法嘗試加鎖,這個加鎖的過程,是直接用CAS操作將state值進行0->1的。如果之間前沒有人加過鎖,那麼state為0,此時線程1加鎖成功

一旦線程加鎖成功後,就可以設置當前線程就是自己。

下圖就是線程1的加鎖過程

其實到這就知道了AQS就是併發包里的一個核心組件,裡面有state變數,加鎖 線程變數等核心東西,維護了加鎖狀態。你會發現ReentrantLock就是一個外面的API,內部的核心鎖機制都是依賴AQS組件的

這個ReentrantLock之所以以Reentrant開頭,意思是它可以可重入鎖。

可重入鎖的意思是,就是你可以對ReentrantLock對象多次執行lock()加鎖和unlock()釋放鎖,也就是可以對一個鎖加多次,叫做可重入加鎖

明白這個後看這個state變數,其實每次線程1可重入加鎖一次,那麼他會判斷當前線程就是自己,那麼他自己就可以沖重入多次加鎖。每次都state+1,別的沒有變化。

線程1加鎖完成後,那麼線程2跑來加鎖會發生什麼呢?

我們看看互斥鎖怎麼實現的,線程2跑來發現state不是0,所以CAS重0->1就失敗,因為不為0說明被加鎖鎖了,那麼就會去看當前加鎖線程是否是自己,不是的話自己就加鎖失敗。

看圖示意:

[MISSING IMAGE: image-20220324170008447, image-20220324170008447 ]

接著,線程2就會將自己放入到AQS的一個等待隊列,因為自己嘗試加鎖失敗了,此時就要將自己放入隊列中等待,等待線程1釋放鎖之後,自己就可以重新嘗試加鎖了。

能夠看到,AQS是如此的核心,AQS內部還有一個等待u隊列,專門放哪些加鎖失敗的線程。

[MISSING IMAGE: image-20220324170416331, image-20220324170416331 ]

接著,線程1在執行完自己的業務後,就會釋放鎖!他釋放鎖的過程很簡單,就是將AQS內部的state變數值遞減到,將“加鎖線程”也是設置為null,徹底釋放鎖了。

接下來,會從等待隊列中喚醒對頭的線程2,線程2重新嘗試加鎖。還是用CAS將state變為1,當前線程為自己線程,同時線程2自己就可以出隊了。

19、線程創建的三種方式

  • 1、Thread類

是Java中表示線程的類,裡面包含了一個線程運行過程中方法run()方法

使用Thread類創建並啟動線程的步驟:

1.寫一個類,繼承Thread類

2.覆蓋Thread類中的run()方法

3.創建線程的實例對象

4.調用線程的實例對象的start()方法去啟動線程

註意:

1.啟動線程調用start()方法

2.啟動線程之後會自動調用run()方法

3.需要線程完成某件事情,將對應的代碼添加到run()方法中即可

  • 2、實現Runnale介面(推薦使用)

步驟:

  1. 寫一個類實現Runnable介面

  2. 實現Runnable介面中的run()

  3. 創建Thread類創建對象,並將第三步創建的對象作為參數傳遞到構造方法中

  4. 調用Thread創建對象的start()方法,啟動線程

採用匿名內部類方式創建線程(固定格式)

public static void main(String[] args) {
    Thread th = new Thread() {
        public void run() {
        System.out.println("匿名內部類的run方法");
        };
     };
    th.start();
}
  • 3、實現Callable介面,並與future

  • 4、線程池結合使用

20、為什麼啟動start(),就調用run方法

關註源碼可以發現,在start()方法中,預設調用了一個JNI方法,這個方法是java平臺用於和本地C代碼進行相互操作的API

21、線程的生命周期

線程的生命周期就是線程的狀態

  • 1新建狀態 new

當使用new關鍵字創建線程實例後,該線程就屬於新建狀態,但是不會執行

  • 2、就緒狀態Runnable

當調用start()方法時,該線程處於就緒狀態,表示可以執行,但是不一定會立即執行,而是等待cpu

分配時間片進行處理

  • 3、運行狀態(Running)

當為該線程分配到時間片後,執行該方法的run方法,就處於運行狀態

  • 4、暫停狀態(包括休眠、等待、阻塞等)(Block)

當線程調用sleep()方法,主動放棄CPU資源,或者線程弔用阻塞IO方法時,比如控制台的Scannner輸入方法

  • 死亡狀態(dead)

當線程的run()方法執行完成之後就處於死亡狀態

註意:

1.當線程創建時,並不會立即執行,需要調用start方法,使其處於就緒狀態

2.線程處於就緒狀態時,也不會立即執行線程,需要等待CPU分配時間

3.當線程阻塞時,會讓出占有的CPU,當阻塞結束時,線程會進入就緒狀態,重新等待CPU,而不是直接進入到運行狀態

  • Thread.yield():

讓出當前CPU時間片,該線程會從運行狀態進入到就緒狀態,此時繼續與其他線程搶占CPU

  • Thread.sleep(time):

讓線程休眠time毫秒,該線程會從運行狀態進入到阻塞狀態,不會與其他線程搶占CPU。當time毫秒過後,該線程會從阻塞狀態進入到就緒狀態,重新與其他線程搶占CPU

非同步的效率會比同步的高,但是非同步存在數據安全問題

多線程併發執行,也就是線程非同步處理,併發執行存線上程安全問題

21、線程安全:

在實際開發中,使用多線程程式的情況很多,如銀行排號系統、火車站售票系統等。這種多線程的程式通常會發生問題,以火車站售票系統為例,在代碼中判斷當前票數是否大於0,如果大於0則執行將該票出售給乘客的功能,但當兩個線程同時訪問這段代碼時(假如這時只剩下一張票),第一個線程將票售出,與此同時第二個線程也已經執行完成判斷 是否有票的操作, 並得出結論票數大於0,於是它也執行售出操作,這樣就會產生負數。所以在編寫多線程程式時,應該考慮到錢程安全問題。實質上線程安全問題來源於兩個線程同時存取單一對象的數據。

線程安全解決問題方案:

1、互斥阻塞同步:也就是加鎖sychronized和ReenrtrantLock,加鎖優缺點?

22、線程同步機制

為了避免多線程的安全問題,需要在公共訪問的內容上加鎖,加鎖之後,當一個線程執行該內容時,其他線程無法執行該內容,只有當該線程將此部分內容執行完了之後,其他線程才可以執行。

  • 1.找到多線程公共執行的內容
  • 2.在此內容上合適的位置加上鎖

鎖:

1、****synchronized可以加在方法上,也可以加在代碼塊中

加在方法上,在返回值前面加synchronized既可,

比如:public synchronized void run() {}表示給run方法整體加上了鎖。
加在代碼塊上:
synchronized(this) {
//需要同步執行的代碼
}

註意:加鎖之後,被加鎖的代碼就變成了同步,會影響效率,所以應該儘量減小加鎖的範圍

2、也可以用RantantLock

23、run()方法和sart()方法有什麼區別

run()方法是線程的執行體,他的方法代表線程需要完成的任務,而start()方法用來啟動線程。

24、線程是否可以被重覆啟動

25、volatile

26、java多線程之間的三種通信方式

1、synchronized來保證線程安全

如果線程之間是通過synchronized來保證線程安全,則可以利用wait()、notify()、notifyAll()來實現通信

2、通過Lock()

如果線程之間是通過Lock()來保證線程安全的,則可以利用await()、signal()、signalAll()來說實現線程通信

這三個方法都是Condition介面中的方法。

3、BlockingQueue

jdk1.5中提供了BlockingQueue介面,雖然四Queue的子介面,但是主要用途並不是作為容器,而是作為線程的通信工具。BlockingQueue具有一個特征:當生產者線程試圖向BlockingQueue中放入一個元素,如果該隊列已滿,則該線程阻塞;

27、說一說synchronized的底層實現原理

一、synchronized作用在代碼塊時,它的底層是通過monitorenter、monitorexit指令來實現的。

  • *monitorenter:

每個對象都是一個監視器鎖(monitor),當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

如果monitor的進入數為0,則該線程進入monitor,然後將進入數設置為1,該線程即為monitor的所有者。如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1。如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

  • *monitorexit:

執行monitorexit的線程必須是objectref所對應的monitor持有者。指令執行時,monitor的進入數減1,如果減1後進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。

monitorexit指令出現了兩次,第1次為同步正常退出釋放鎖,第2次為發生非同步退出釋放鎖。

二、方法的同步並沒有通過 monitorenter 和 monitorexit 指令來完成,不過相對於普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符。JVM就是根據該標示符來實現方法的同步的:

當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。

三、總結

兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。兩個指令的執行是JVM通過調用操作系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響

28、CAS

1、概念

2、CAS可能產生ABA問題:

   ABA解決問題:加一個版本號

   版本號:數值型或者布爾型

29、鎖升級初步

  • new->偏向鎖->輕量級鎖(無鎖、自旋鎖、自適應自旋3)->重量級鎖

1、偏向鎖:

在鎖對象的對象頭中記錄⼀下當前獲取到該鎖的線程ID,該線程下次如果⼜來獲取該鎖就

2、輕量級鎖

由偏向鎖升級⽽來,當⼀個線程獲取到鎖後,此時這把鎖是偏向鎖,此時如果有第⼆個 線程來競爭鎖,偏向鎖就會升級為輕量級鎖,之所以叫輕量級鎖,是為了和重量級鎖區分開來,輕 量級鎖底層是通過⾃旋來實現的,並不會阻塞線程

3、鎖重入鎖

sychnronized必須記錄重入次數,因為要解鎖必須對應次數

偏向鎖 自旋鎖 ->線程棧->LR+1

4、自旋鎖什麼時候升級為重量級鎖

競爭加劇:有線程超過10次,或者自旋鎖線程數超過CPU核數的一半,1.6以後加入自適應自旋,JVM自己控

5、為什麼有自旋鎖還需要重量級鎖

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

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

6、偏向鎖是否一定比自旋鎖效率高

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

JVM啟動過程,會有很多線程競爭(明確),所以預設情況下啟動是不會啟動偏向鎖,過一會時間再打開

30、ThreadLocal機制

對於ThreadLocal而言,常用的方法,就是get/set/initialValue方法。

ThreadLocal提供一個線程(Thread)局部變數,訪問到某個變數的每一個線程都擁有自己的局部變數。說白了,ThreadLocal就是想在多線程環境下去保證成員變數的安全。

你會看到,set需要首先獲得當前線程對象Thread;

然後取出當前線程對象的成員變數ThreadLocalMap;

如果ThreadLocalMap存在,那麼進行KEY/VALUE設置,KEY就是ThreadLocal;

如果ThreadLocalMap沒有,那麼創建一個;

說白了,當前線程中存在一個Map變數,KEY是ThreadLocal,VALUE是你設置的值。

31、ThreadLocal機制的記憶體泄露

首先來說,如果把ThreadLocal置為null,那麼意味著Heap中的ThreadLocal實例不在有強引用指向,只有弱引用存在,因此GC是可以回收這部分空間的,也就是key是可以回收的。但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷毀時,value才能得到釋放。

因此,只要這個線程對象被gc回收,就不會出現記憶體泄露,但在threadLocal設為null和線程結束這段時間內不會被回收的,就發生了我們認為的記憶體泄露。最要命的是線程對象不被回收的情況,比如使用線程池的時候,線程結束是不會銷毀的,再次使用的,就可能出現記憶體泄露。

那麼如何有效的避免呢?

事實上,在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那麼是會對value置為null的。我們也可以通過調用ThreadLocal的remove方法進行釋放!

留言:

這是本人今年春招找實習工作准備總結,記錄在此,如有需要的老鐵可以看看,如有問題可以留言指導


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 整數類型 標準 SQL 中支持 INTEGER 和 SMALLINT 這兩種類型,MySQL 資料庫除了支持這兩種類型以外,還擴展支持了 TINYINT、MEDIUMINT 和 BIGINT 整數類型 位元組數 無符號數的取值範圍 有符號數的取值範圍 TINYINT 1 (0,255) (-128,1 ...
  • 一、Spark概述 Spark基礎概念和原理講解可以參考我上篇博文:大數據Hadoop之——計算引擎Spark 二、Spark的運行模式 1)Standalone(本章講解) 獨立模式,自己獨立一套集群(master/client/slave),Spark 原生的簡單集群管理器, 自帶完整的服務, ...
  • 【導讀】 拍攝、導入、特效、卡點、BGM…幾步簡單的操作,我們便可將生活的瞬間用視頻記錄與分享。應用前沿AI技術,提供一站式視頻處理能力,幫助開發者們構建更智能、更易用、更專業的視頻剪輯軟體,打造視頻趣味新玩法。 HMS Core Discovery第14期直播將在2022年4月21日19:00開播 ...
  • 請問昨天結束的早是對堆積在了今天嗎,今天還來加個班更博,看在這個毅力的份上能否給億點點推薦。 有個好消息有個壞消息,先說壞消息吧,就是在這麼學下去我急需急支糖漿,來回顧回顧前面的知識,這幾天學的太急了,搞得有點推著走的意思,好消息就是今天的內容是最後最後node的基礎內容了,果然天不負我,整完然後有 ...
  • ##背景 封樓期間難得空閑,也靜不下心學習,空閑之餘萌生了重做引導單頁的想法。因為之前都是扒站(某大公司游戲官網)+小改,一來雖然很炫酷,但本人水平有限,仍有很大一部分JS無從下手,甚至是看不懂|-_-|;二來對方畢竟沒有開源,無論道德還是法律都說不過去,所以……先從簡單處寫起,後續慢慢迭代吧! # ...
  • 責任鏈模式是什麼 責任鏈模式是一種行為設計模式, 允許你將請求沿著處理者鏈進行發送。 收到請求後, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。 為什麼要用責任鏈模式 如果有多個對象可以處理同一個請求,具體哪個對象處理該請求由運行時刻自動確定。或者所需處理者及其順序必須在運行時進行 ...
  • 一、Spring事件監聽介紹 Spring對事件監聽是通過事件類型、事件類型監聽和事件發佈器3個部分來完成的 // 1. 自定義訂單事件 public class OrderEvent extends ApplicationEvent { ... } // 2. 定義訂單監聽器 @Component ...
  • Apache Flink系列-①什麼是Apache Flink? Apache Flink是一個框架和分散式處理引擎,用於在無界和有界數據流上進行有狀態計算。Flink被設計為在所有常見的集群環境中運行,以記憶體速度和任何規模執行計算。 在這裡,我們解釋Flink’s架構重要的各個方面。 處理無界和有 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...