剖析面試最常見問題之Java 併發知識進階(下)

點關注,不迷路;持續更新Java相關技術及資訊!!!

剖析面試最常見問題之Java 併發知識進階(下)

1. 線程池

1.1. 爲什麼要用線程池?
線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java 併發編程的藝術》提到的來說一下使用線程池的好處:

  • 降低資源消耗。 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。 當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

推薦:博主的一個【java技術交流小站】:點擊進入 暗號csdn

羣號:530720915 進羣驗證“csdn”
分享Java中高級開發進階資料、源碼、筆記、視頻、Dubbo、Redis、Netty、zookeeper、Springcloud、分佈式、高併發等架構技術架構教程

1.2. 實現 Runnable接口和 Callable接口的區別
如果想讓線程池執行任務的話需要實現的 Runnable 接口或 Callable 接口。 Runnable 接口或 Callable 接口實現類都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 執行。兩者的區別在於 Runnable 接口不會返回結果但是 Callable 接口可以返回結果。

備註: 工具類Executors可以實現Runnable對象和Callable對象之間的相互轉換。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

1.3. 執行 execute() 方法和 submit() 方法的區別是什麼呢?
1)execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;

2)submit() 方法用於提交需要返回值的任務。線程池會返回一個 Future 類型的對象,通過這個 Future 對象可以判斷任務是否執行成功,並且可以通過 future 的 get() 方法來獲取返回值,get() 方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

1.4. 如何創建線程池
《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Executors 返回線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
  • CachedThreadPool 和 -ScheduledThreadPool : 允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。

方式一:通過構造方法實現
在這裏插入圖片描述
方式二:通過 Executor 框架的工具類 Executors 來實現
我們可以創建三種類型的 ThreadPoolExecutor:

  • FixedThreadPool : 該方法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池中若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閒時,便處理在任務隊列中的任務。
  • SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。
  • CachedThreadPool: 該方法返回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務執行完畢後,將返回線程池進行復用。
    對應 Executors 工具類中的方法如圖所示:
    在這裏插入圖片描述

2. Atomic 原子類

2.1. 介紹一下 Atomic 原子類
Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裏 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。

併發包 java.util.concurrent 的原子類都存放在java.util.concurrent.atomic下,如下圖所示。
在這裏插入圖片描述
2.2. JUC 包中的原子類是哪 4 類?
基本類型

使用原子的方式更新基本類型

  • AtomicInteger:整形原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean:布爾型原子類

數組類型

使用原子的方式更新數組裏的某個元素

  • AtomicIntegerArray:整形數組原子類

  • AtomicLongArray:長整形數組原子類

  • AtomicReferenceArray:引用類型數組原子類
    引用類型

  • AtomicReference:引用類型原子類

  • AtomicStampedRerence:原子更新引用類型裏的字段原子類

  • AtomicMarkableReference :原子更新帶有標記位的引用類型
    對象的屬性修改類型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

  • AtomicLongFieldUpdater:原子更新長整形字段的更新器

  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

2.3. 講講 AtomicInteger 的使用

AtomicInteger 類常用方法

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲 newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。

AtomicInteger 類的使用示例

使用 AtomicInteger 之後,不用對 increment() 方法加鎖也可以保證線程安全。

class AtomicIntegerTest {
        private AtomicInteger count = new AtomicInteger();
      //使用 AtomicInteger 之後,不需要對該方法加鎖,也可以實現線程安全。
        public void increment() {
                  count.incrementAndGet();
        }

       public int getCount() {
                return count.get();
        }
}

2.4. 能不能給我簡單介紹一下 AtomicInteger 類的原理
AtomicInteger 線程安全原理簡單分析

AtomicInteger 類的部分源碼:

  // setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較並替換”的作用)
    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 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大爲提升。

CAS 的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內存地址,返回值是 valueOffset。另外 value 是一個 volatile 變量,在內存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。

3. AQS

3.1. AQS 介紹
AQS 的全稱爲(AbstractQueuedSynchronizer),這個類在 java.util.concurrent.locks 包下面。
在這裏插入圖片描述
AQS 是一個用來構建鎖和同步器的框架,使用 AQS 能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的 ReentrantLock,Semaphore,其他的諸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基於 AQS 的。當然,我們自己也能利用 AQS 非常輕鬆容易地構造出符合我們自己需求的同步器。

3.2. AQS 原理分析

在面試中被問到併發知識的時候,大多都會被問到“請你說一下自己對於 AQS
原理的理解”。下面給大家一個示例供大家參加,面試不是背題,大家一定要假如自己的思想,即使加入不了自己的思想也要保證自己能夠通俗的講出來而不是背出來。

下面大部分內容其實在 AQS 類註釋上已經給出了,不過是英語看着比較吃力一點,感興趣的話可以看看源碼。

3.2.1. AQS 原理概覽
AQS 核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制 AQS 是用 CLH 隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

CLH(Craig,Landin,and Hagersten)
隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS 是將每條請求共享資源的線程封裝成一個 CLH
鎖隊列的一個結點(Node)來實現鎖的分配。

看個 AQS(AbstractQueuedSynchronizer) 原理圖:
在這裏插入圖片描述
AQS 使用一個 int 成員變量來表示同步狀態,通過內置的 FIFO 隊列來完成獲取資源線程的排隊工作。AQS 使用 CAS 對該同步狀態進行原子操作實現對其值的修改。

private volatile int state;//共享變量,使用 volatile 修飾保證線程可見性

狀態信息通過 protected 類型的 getState,setState,compareAndSetState 進行操作

//返回同步狀態的當前值
protected final int getState() {  
        return state;
}
 // 設置同步狀態的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS 操作)將同步狀態值設置爲給定值 update 如果當前同步狀態的值等於 expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

3.2.2. AQS 對資源的共享方式

AQS 定義兩種資源共享方式

  • Exclusive(獨佔):只有一個線程能執行,如 ReentrantLock。又可分爲公平鎖和非公平鎖:
    • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。

ReentrantReadWriteLock 可以看成是組合式,因爲 ReentrantReadWriteLock 也就是讀寫鎖允許多個線程同時對某一資源進行讀。

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS 已經在頂層實現好了。

3.2.3. AQS 底層使用了模板方法模式

同步器的設計是基於模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經典的一個應用):

1.使用者繼承 AbstractQueuedSynchronizer 並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源 state 的獲取和釋放)
2. 將 AQS 組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。

這和我們以往通過實現接口的方式有很大區別,這是模板方法模式很經典的一個運用。

AQS 使用了模板方法模式,自定義同步器時需要重寫下面幾個 AQS 提供的模板方法:

isHeldExclusively()//該線程是否正在獨佔資源。只有用到 condition 才需要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回 true,失敗則返回 false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回 true,失敗則返回 false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0 表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回 true,失敗則返回 false。

默認情況下,每個方法都拋出 UnsupportedOperationException。 這些方法的實現必須是內部線程安全的,並且通常應該簡短而不是阻塞。AQS 類中的其他方法都是 final ,所以無法被其他類使用,只有這幾個方法可以被其他類使用。

以 ReentrantLock 爲例,state 初始化爲 0,表示未鎖定狀態。A 線程 lock() 時,會調用 tryAcquire() 獨佔該鎖並將 state+1。此後,其他線程再 tryAcquire() 時就會失敗,直到 A 線程 unlock() 到 state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放鎖之前,A 線程自己是可以重複獲取此鎖的(state 會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證 state 是能回到零態的。

再以 CountDownLatch 以例,任務分爲 N 個子線程去執行,state 也初始化爲 N(注意 N 要與線程個數一致)。這 N 個子線程是並行執行的,每個子線程執行完後 countDown() 一次,state 會 CAS(Compare and Swap) 減 1。等到所有子線程都執行完後 (即 state=0),會 unpark() 主調用線程,然後主調用線程就會從 await() 函數返回,繼續後餘動作。

一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一種即可。但 AQS 也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock

3.3. AQS 組件總結

  • Semaphore(信號量)-允許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量) 可以指定多個線程同時訪問某個資源。
  • CountDownLatch (倒計時器): CountDownLatch 是一個同步工具類,用來協調多個線程之間的同步。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。
  • CyclicBarrier(循環柵欄): CyclicBarrier 和 CountDownLatch 非常類似,它也可以實現線程間的技術等待,但是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 類似。CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。CyclicBarrier 默認的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用 await() 方法告訴 CyclicBarrier 我已經到達了屏障,然後當前線程被阻塞。

希望本文對大家有所幫助,一起進步,喜歡點個贊 評論 點下關注唄 以後將不斷持續更新~

【java技術交流小站】:點擊進入 暗號csdn

上文推薦 :https://blog.csdn.net/weixin_44092679/article/details/94740171

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章