《Java核心技術》閱讀筆記(四)- 併發

併發編程基礎

線程基本概念

多線程與多進程的區別: 每個進程擁有自己的一整套變量,而線程則共享數據。
並行運行多個任務

創建

  1. new Thread(Runnable);
  2. 繼承Thread的創建方式不再推薦,應該將並行運行的任務與運行機制解耦合;
  3. 線程池:當任務很多時,爲每個任務創建一個獨立的線程付出代價太大。(14.9)

注:Callabe類似於Runnable接口,表示具有返回值的異步任務。FutureTask類實現了Runnable、Funture接口,可用於Callabe與Runnable、Futrue之間的轉換。

啓動

thread.start()

中斷

沒有強制線程終止的方法,stop與suspend方法已經不推薦使用,interrupt方法可用來請求終止線程。

一般情況,線程要時不時的檢測interrupt狀態。

while (!Thread.currentThread().islnterrupted() && more work to do) {
	do more work
}

當在一個被阻塞的線程(調用 sleep 或 wait) 上調用 interrupt方法時,阻塞調用將會被Interrupted Exception異常中斷。(也存在不會被中斷的阻塞I/O調用,應該考慮選擇可中斷的調用)。

被中斷的線程可自行決定如何響應中斷,普遍情況下是終止線程:

Runnable r = () -> {
	try
	{
		. . . 
		while (!Thread.currentThread().isInterrupted() && more work to do) {
			do more work
		} 
	}
	catch(InterruptedException e) { 
		// thread was interrupted during sleep or wait
	}
	finally
	{
		cleanup,if required
	}
	// exiting the run method terminates the thread
};

注:如果在循環體內調用sleep方法,當中斷狀態被置位時,它不會休眠,相反,會清除這一狀態,並拋出異常。因此,此時在while循環處沒有必要進行isInterrupted的檢測,刪除檢測狀態的代碼即可。

Thread.interrupted() 和 thread.isInterrupted 比較:靜態方法會清除中斷狀態。

不要忽略中斷異常,更好的處理方式:

  1. sleep方法被中斷時會清除狀態,因此可在異常處理中重新設置狀態,以便調用方進行檢測

    void mySubTask()
    {
        ……
        try { sleep(delay); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        ……
    }
    
  2. throws InterruptedException標記方法
    void mySubTask() throws InterruptedException

狀態getState()

  1. New
    還沒有開始運行
  2. Runnable
    可能沒有在運行,取決於操作系統調度機制(搶佔式-時間片、協作式)
  3. Blocked、Waiting、Timed waiting
    • 試圖獲取一個內部對象鎖,而該鎖被其他線程持有,阻塞;
    • 當線程等待另一個線程通知調度器一個條件時(wait、join或是等待Lock或Condition時),等待;
    • wait、join、tryLock、await、sleep 帶有超時參數的方法被調用時,進入計時等待。
  4. Terminated
    • run方法正常退出
    • 沒有捕獲的異常意外死亡
    • stop方法(過時)

被阻塞狀態與等待狀態有哪些不同?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gt0NmIDZ-1582987902984)(evernotecid://1520493E-927F-420A-8EE1-BA6F74088A9D/appyinxiangcom/11767354/ENResource/p3036)]

屬性

  1. 優先級
    高度依賴於系統:1 ~ 10 映射到OS時有可能更多,也有可能更少。Oracle爲Linux提供的Java虛擬機中,線程的優先級被忽略。

不要將程序構建爲功能的正確性依賴於優先級。
調度器優先選擇高優先級線程:若高優先級線程沒有進入非活躍狀態,則低級別線程永遠不能執行
Thread.yield() 讓出CPU

  1. 守護線程setDaemon
    –爲其他線程提供服務(示例:計時線程),只剩下守護線程時,虛擬機退出。
    –使用:不要訪問固有資源,如文件、數據庫,它會在任何時候發生中斷。
    在線程啓動之前調用setDaemon(true)

  2. Thread.UncaughtExceptionHandler
    setUncaughtExceptionHandler
    如果有父線程組(ThreadGroup),調用父線程組這一方法;否則,若Thread對象有默認處理器則調用;否則,如果Throwable是ThreadDeath的一個實例(stop方法產生),什麼都不做;否則,輸出棧軌跡到標準錯誤流。

線程組是一個可以統一管理的線程集合。默認情況下,創建的所有線程屬於相同的線程組, 但是, 也可能會建立其他的組。現在引入了更好的特性用於線程集合的操作,所以建議不要在自己的程序中使用線程組。

同步

如何控制線程之間的交互

鎖和條件

競爭條件race condition(多個線程修改相同對象產生訛誤的對象)
出現原因:方法的執行過程可能被中斷(非原子操作)(javap -c -v classname可查看類的字節碼,其中可能一條java語句生成多條虛擬機指令,運行可能被中斷,而造成訛誤的對象出現)

解決:鎖和條件(Lock/Condition 或 synchronized)

  1. Lock / ReentrantLock
    基本使用結構:

    private Lock bankLock = new ReentrantLock();  //object field
    
    myLock.lock(); // a ReentrantLock object, a share object,second thread whill bolcked
    try
    {
        critical section
    }
    finally
    {
        myLock.unlock()// make sure the lock is unlocked even if an exception is thrown
    }
    

    注:如果使用鎖,就不能使用帶資源的try語句,帶資源的try語句希望首部聲明新變量,而lock,我們要使用多線程共享的那個變量;
    注:注意編寫臨界區內的代碼,避免因爲拋出異常跳出臨界區,造成訛誤對象的出現。

    可重入的鎖保持一個持有計數hold count,記錄調用lcok方法的嵌套調用次數。被一個鎖保護的代碼,可以調用另一個使用相同鎖保護的方法。

    公平鎖(帶參數fair的構造方法)
    公平鎖認爲等待時間越長的線程越應該得到執行,但這會影響性能,公平鎖默認是不公平的。只有確定自己需要使用公平鎖的需求時,纔可以考慮公平鎖,即使使用了,線程調度器也有可能選擇忽略一個線程,而這個線程已經等待了鎖很長時間的情況。

  2. 條件對象 / 條件變量
    爲什麼需要條件對象?
    獲得鎖並進入臨界區的線程,發現某個條件滿足後才能繼續執行,需要等待另一個線程修改了共享對象狀態後,再進行檢測條件,滿足了再執行。
    舉例:如果一個轉賬操作獲得了鎖,臨界區內,在轉賬之前檢測到餘額不足,這時需要等待其他線程先進行轉賬,待餘額充足後再繼續執行。一個鎖對象,可以有多個相關的條件對象。

    使用:
    ① sufficientFunds = lock.newCondition(); // 返回一個與鎖相關的條件對象
    ② 不滿足條件,需要阻塞時調用sufficientFunds.await(),執行線程阻塞,釋放鎖,寄希望於其他線程;
    ③ 等待其他線程調用同一條件的signalAll方法。
    signalAll() – 解除該條件等待集中所有線程的阻塞狀態,不會立即激活一個等待線程,以便這些線程可以在當前線程退出同步方法之後,通過競爭實現對對象的訪問。
    調用時機:在對象的狀態有利於等待線程的方向改變時調用。例如轉賬完成,賬戶餘額發生變化時。

等待獲得鎖的線程和調用await方法的線程存在本質上的不同。一旦一個線程調用await方法, 它進人該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處於阻塞狀態,直到另一個線程調用同一條件上的signalAll方法時爲止。

重新獲得鎖的線程需要重新檢測是否滿足條件。一般調用await的方式:

while(!(ok to proceed))
	condition.await();	// 將線程放到條件的等待集中

另一個signal方法,隨機解除等待集中某個線程的狀態,更加有效,但存在危險:有可能基礎阻塞的線程仍然不能運行

同步機制中的簿記操作付出的代價,程序運行可能會慢。正確使用條件對象富有挑戰性,在實現自己的條件對象之前優先考慮使用同步器相關結構。

小結:Lock與Condition對象
	• 鎖用來保護代碼片段, 任何時刻只能有一個線程執行被保護的代碼。 
	• 鎖可以管理試圖進入被保護代碼段的線程。 
	• 鎖可以擁有一個或多個相關的條件對象。 
	• 每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
  1. synchronized
    每一個對象都有一個內部鎖,並且該鎖有一個內部條件。
    wait、notifyAll、notify 等同於 await、signalAll、signal

    也可聲明靜態方法,調用方法獲得 類對象 的內部鎖

    優勢:代碼簡潔,不易出錯
    侷限:
    • 不能中斷一個正在試圖獲得鎖的線程。
    • 試圖獲得鎖時不能設定超時。
    • 每個鎖僅有單一的條件,可能是不夠的。

    應該使用 Lock/Condition 還是 synchronized ?
    • 最好既不使用Lock/Condition也不使用 synchronized 關鍵字。在許多情況下可以使用 java.util.concurrent 包中的一種機制,它會爲你處理所有的加鎖。
    • 如果 synchronized 關鍵字適合你的程序, 那麼請儘量使用它,這樣可以減少編寫的代碼數量,減少出錯的機率。
    • 如果特別需要 Lock/Condition 結構提供的獨有特性時,才使用 Lock/Condition。

    同步阻塞:synchronized(obj){}
    使用一個對象的鎖實現額外的原子性操作,稱爲客戶端鎖定(client side locking),依賴於內部實現阻塞的事實,因此這個機制是脆弱的。

安全訪問共享域

  1. volatile
    問題:多線程讀寫同一個實例域出現不一致的問題
  • 寄存器或本地內存緩衝區中保存內存中的值,不同處理器線程在同一內存位置取到不同的值。
  • 改變指令執行順序使吞吐量最大化

volatile爲實例域的同步訪問提供了免鎖機制。如果聲明一個域爲 volatile ,那麼編譯器和虛擬機就知道該域是可能被另一個線程併發更新的。
volatile變量不能保證原子性,不能保證讀取、翻轉和寫入不被中斷。

“ 如果向一個變量寫入值, 而這個變量接下來可能會被另一個線程讀取, 或者,從一個變量讀值, 而這個變量可能是之前被另一個線程寫入的, 此時必須使用同步 “–同步格言。

  1. final變量
    final Map<String, Double> accounts = new HashKap<>();

  2. 原子性

  • 只是賦值操作,可使用volatile
  • 原子方式設置或增減值操作,可使用automic包下的一些類(提供了機器級指令)
  • 要完成更復雜的更新,可使用compareAndSet(例如:跟蹤不同線程觀察的最大值)
public static AtonicLong largest = new AtomicLong()do {
    oldValue = largest.get();
    newValue = Math.max(oldValue , observed); 
} while (largest.compareAndSet(oldValue, newValue));

注:compareAndSet方法會映射到一個處理器操作,比使用鎖速度更快。
Java8以後的簡化寫法:

largest. updateAndGet(x -> Math .max(x, observed));1argest.accumulateAndCet(observed , Math::max);
(getAndUpdate 和 getAndAccumulate可以返回原值)

如果有大量線程要訪問相同的原子值,性能會大幅下降,因爲樂觀更新需要太多次重試(樂觀鎖的機制)。Java SE 8 提供了 LongAdder 和 LongAccumulator 類。
LongAdder提供多個加數(變量)對應不同線程,方法:increment、sum
LongAccumulator 將這種思想推廣到任意的累加操作。accumulate、get(需要滿足交換律與結合律)
(類似的類:DoubleAdder、DoubleAccumulator)

死鎖

有可能出現每一個線程要等待條件對象被激活的情況,導致了所有線程都被阻塞,這樣的狀態被稱爲死鎖(dead lock)。
通過jconsole觀察死鎖線程的調用棧,必須仔細設計線程,確保不會出現死鎖。(signal可能導致死鎖)

線程局部變量ThreadLocal

public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
dateFormat.get().format(new Date());

int random = ThreadLocalRandom.curren().nextlnt(upperBound):

鎖測試與超時

lock方法不能被中斷。如果一個線程在等待獲得一個鎖時被中斷,中斷線程在獲得鎖之前一直處於阻塞狀態。如果出現死鎖,那麼,lock方法就無法終止。
然而,如果調用帶有用超時參數的tryLock,那麼如果線程在等待期間被中斷,將拋出
InterruptedException 異常。這是一個非常有用的特性,因爲允許程序打破死鎖。

也可以調用locklnterruptibly方法。它就相當於一個超時設爲無限的tryLock方法。

tryLock(time)、await(time) 時間到了返回false、awaitUninterruptibly()

ReentrantReadWriteLock

.readLock() //得到一個可以被多個讀操作共用的讀鎖,但會排斥所有寫操作。
.writeLock() //得到一個寫鎖,排斥所有其他的讀操作和寫操作。

stop、suspend、resume棄用原因

  1. stop棄用原因:停止線程可能導致對象處於損壞的狀態,無法知道何時調用stop方法是安全的,什麼時候會導致對象被破壞。ThreadDeath異常將釋放所有鎖對象
  2. suspend棄用原因:容易造成死鎖。如果suspend掛起一個持有鎖的線程,而調用suspend方法的線程也要獲得同一個鎖,則程序死鎖。

線程安全的集合

阻塞隊列BlockingQueue

方法(按照隊列滿或者空時的響應方式)
	① 操作阻塞:put、take方法(作線程管理工具)
	② 拋異常:add、remove、element
	③ 錯誤提示:offer、poll、peek(多線程操作)
實現類:
	ArrayBlockingQueue(int capacity)
	LinkedBlockingQueue()    // 默認無限,容量可選
	DelayQueue()    // 延遲已經超過時間的元素可以從隊列中移出
	PriorityBlockingQueue()    // 優先級隊列
接口:
	BlockingQueue
	BiockingDeque
	TransferQueue

高效的映射、集合隊列

  1. ConcurrentHashMap

    • 原子更新:
      循環執行replace方法 或
      map.putlfAbsent(word, new LongAdder()).increment();
      map.compute(word , (k, v) -> v = null ? 1: v + 1);
      map.computelfAbsent(word , k -> new LongAdder()).increment();
      map.merge(word, 1L, (existingVal, newVal) -> existingVal + newVal);
      注:compute、merge參數中的函數返回null,則會從map中刪除現有條目。且函數不要做太多工作,可能會阻塞對映射的其他更新,且不能更新映射的其他部分。

    • 批操作:映射狀態的一個近似
      search、forEach、reduce(Keys、Values、KV、Entries)

      1. 可指定參數化閾值,映射包含元素多於閾值,會並行完成批操作(1 ~ Long.MAX_VALUE)
      2. forEach、reduce可指定一個轉換函數,結果爲null時可實現過濾效果
      3. 原始類型特化
    • 併發Set視圖
      Set words = ConcurrentHashMap.newKeySet();
      keySet方法可生成現有映射的鍵集,能夠刪除,不能增加元素;重載keySet(defaultVal)可增加元素。

  2. 寫數組拷貝
    CopyOnWriteArrayList 和 CopyOnWriteArraySet

    • 使用場景:迭代線程數超過修改線程數
    • 一致性:可能過時的一致性
  3. 同步包裝器

    • 在另一個線程可能進行修改時要對集合進行迭代時,仍然需要使用“客戶端”鎖定,若在迭代過程中,別的線程修改集合,迭代器會失效,拋出ConcurrentModificationException異常,同步仍然是需要的,併發的修改可以被可靠地檢測出來;
    • 最好使用java.util.concurrent包中定義的集合,不使用同步包裝器中的。特別是,假如它們訪問的是不同的桶,由於ConcurrentHashMap已經精心地實現了,多線程可以訪問它而且不會彼此阻塞。有一個例外是經常被修改的數組列表。在那種情況下,同步的ArrayList可以勝過CopyOnWriteArrayList

    ConcurrentSkipListMap
    ConcurrentSkipListSet
    ConcurrentSkipListQueue

執行器(Executor)

線程池

爲什麼使用線程池?

  1. 構建新線程,涉及與操作系統交互,用於創建創建大量生命週期很短的線程;
    (如果有很多任務, 要爲每個任務創建一個獨立的線程所付出的代價太大了)
  2. 減少併發線程的數目

Executors:
創建線程池,返回ExecutorService接口的實現類ThreadPoolExecutor的對象

使用流程:
1. 調用 Executors 類中靜態的方法 newCachedThreadPool 或 newFixedThreadPool。
2. 調用 submit 提交 Runnable 或 Callable 對象。
3. 如果想要取消一個任務, 或如果提交 Callable 對象, 那就要保存好返回的 Future 對象。
4. 當不再提交任何任務時,調用 shutdown。

控制任務組

  1. shutdownNow
  2. invokeAny
  3. invokeAll
  4. ExecutorCompletionService將結果按可獲得的順序保存起來

Fork-Join框架

  • 針對每個處理內核使用一個線程完成計算密集型任務的應用使用
  • RecursiveTask
  • RecursiveAction
    compute、invokeAll、join、get

注:框架使用了”工作密取(work stealing)“的方法來平衡可用線程的工作負載

可完成Future

可以組合(composed),指定執行順序

  1. 單個future的方法:
  • thenApply
  • thenCompose
  • handle
  1. 組合多個future的方法:
  • thenCombine
  • runAfterBoth
  • applyToEither
  • allOf
  • anyOf

同步器

  1. 信號量
    類:Semaphore(acquire、release)
    作用:允許線程集等待,直到被允許繼續運行爲止
    場景:限制訪問資源的線程總數

  2. 倒計時門栓
    類:CountDownLatch
    作用:允許線程集等待,直到計數器減爲0
    場景:當一個或多個線程等待,直到指定數量的事件發生
    一次性,不能重複使用,例如初識數據準備

  3. 障柵
    類:CyclicBarrier barrier = new CydicBarrier(nthreads); 每個線程,執行到障柵時調用await,障柵動作可選。(Phaser類更靈活)
    作用:允許線程集等待直至其中預定數目的線程到達一個公共障柵( barrier,) 然後可以選擇執行一個處理障柵的動作
    場景:當大量的線程需要在它們的結果可用之前完成時

  4. 交換器
    類:Exchanger
    作用:允許兩個線程在要交換對象準備好時交換對象。
    場景:當兩個線程工作在同一數據結構的兩個實例上的時候, 一個向實例添加數據而另一個從實例清除數據。

  5. 同步隊列
    類:synchronousQueue(put方法將阻塞,直到另一個線程take爲止,反之亦然,size永爲0)
    作用:允許一個線程把對象交給另一個線程
    場景:在沒有顯式同步的情況下, 當兩個線程準備好將一個對象從一個線程傳遞到另一個時

小結

本文整理了Java併發基礎相關知識,從線程的創建、啓動、中斷開始,到線程間的交互和協調,以及Java類庫提供的同步工具、線程安全集合、線程池等內容,爲併發程序的開發提供基礎儲備。

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