知識點(Java併發編程實戰)

讀書記錄的一些知識點和部分其他資料的參考和理解,細節內容請參考其他資料

線程安全性

什麼是線程安全性

當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就稱這個類是線程安全的。

原子性

競態條件

1、當某個計算的正確性取決於多個線程的交替執行時序時,就會發生競態條件。換句話說就是正確的結果取決於運氣。

2、最常見的競態條件類型就是“先檢查後執行(Check-Then-Act)操作,即通過一個可能失效的觀測結果來決定下一步的動作。

示例:延遲初始化中的競爭態條件

1、使用“先檢查後執行” 的一種常見情況就是延遲初始化。

2、延遲初始化的目的是將對象的初始化操作推遲到實際被使用時才進行,同時要確保只被初始化一次。

3、競態條件很容易與“數據競爭”相混淆。數據競爭是指,如果在訪問非享的非final類型的域時沒有采用同步來進行協同,那麼就會出現數據競爭。並非所有的競態條件都是數據競爭,同樣並非所有的數據競爭都是競態條件。

複合操作

加鎖機制

要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量。

內置鎖 (Synchronized的使用)

1、java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)

2、同步代碼塊包括兩部分:一個作爲鎖的對象引用,一個作爲由這個鎖保護的代碼塊。以關鍵字synchronuzed來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。靜態的synchronized方法以Class對象作爲鎖。

3、每個Java對象都可以用作一個實現同步的鎖,這些鎖被稱爲內置鎖(Intrinsic Lock)或監視器鎖(Monitor Lcok)。

4、sycnhronized的三種應用方式:

Java中每一個對象都可以作爲鎖,這是synchronized實現同步的基礎:

  • 普通同步方法(實例方法),鎖是當前實例對象 ,進入同步代碼前要獲得當前實例的鎖
  • 靜態同步方法,鎖是當前類的class對象 ,進入同步代碼前要獲得當前類對象的鎖
  • 同步方法塊,鎖是括號裏面的對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

重入

1、由於內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。

2、重入意味着獲取鎖的操作的粒度是“線程”而不是“調用”。

3、重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個所有者線程。

4、這與pthread(POSIX線程)互斥體的默認加鎖行爲不通,pthread互斥體的獲取操作是以“調用”爲力度的。

用鎖來保護狀態

1、由於鎖能使其保護的代碼路徑以串型形式來訪問,因此可以通過鎖來構造一些協議以實現對共享狀態的獨佔訪問。

2、一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態的代碼路徑進行同步,使得在改對象上不會發生併發訪問。

活躍性與性能

對象的共享

可見性

/**
 * @description: 可見性
 * @author: dsy
 * @date: 2020/3/23 13:05
 */
public class NoVisibility {

    private static boolean ready;

    private static int number;

    private static class ReaderThread extends Thread{
        @Override
        public void run(){
            while (ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args){
        new ReaderThread().start();
        number = 4;
        ready = true;
    }
}

NoVisibility可能會持續循環下去,因爲讀線程可能永遠看不到ready的值;也可能會輸出0,因爲度線程可能看到了寫入ready的值,但卻沒看到之後寫入number的值,這種線程被稱爲“重排序”。

失效數據

非原子的64位操作

1、當線程在沒有同步的情況下讀取變量時,可能會得到一個失效的值,但至少這個值是由之前某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性。

2、最低安全性適用於絕大多數變量,但是存在一個例外:非volatile類型的64位數值變量(double和long)。Java內存模型要求,變量的讀取操作和寫入操作都必須是原子操作,但對於非volatile類型的long和double變量,JVM允許將64位的讀操作或寫操作分解爲兩個32位的操作。

加鎖與可見性

Volatile變量(可見性原理利用了MESI–緩存一致性協議,禁止重排序利用了內存屏障

1、當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。

2、volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方。

3、調試小提示:對於服務器應用程序,無論在開發階段還是在測試階段,當啓動JVM時一定都要指定-server命令行選項。server模式的JVM將比client模式的JVM進行更多的優化。

4、當且僅當滿足以下所有條件時,才應該使用volatile變量:

  • 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
  • 該變量不會與其他狀態變量一起納入不變性條件中。
  • 在訪問變量時不需要加鎖。

發佈與逸出

1、“發佈(Publish)”一個對象的意思是指,使對象能夠再當前作用域之外的代碼中使用。

2、當某個不應該發佈的對象被髮布時,這種情況就被稱爲逸出(Escape)。

3、安全的對象構造過程:不要在構造過程中使this引用逸出。

線程封閉

1、如果僅在單線程內訪問數據,就不需要同步,這種技術被稱爲線程封閉。

2、線程封閉技術的一種常見應用是JDBC(Java Database Connectivity)的Connection對象。

3、線程封閉是在程序設計種的一個考慮因素,必須再程序中實現,Java語言及其核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocak類。但即便如此。但即便如此,程序員仍然需要負責確保封閉在線程中的對象不會從線程中逸出。

Ad-hoc線程封閉

1、Ad-hoc線程封閉是指,維護線程封閉性的職責完全由程序實現來承擔。

2、由於Ad-hoc線程封閉技術的脆弱性,在程序種儘量少用,可以使用更強的線程封閉技術,如棧封閉或者ThreadLocal類。

棧封閉

1、棧封閉是線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象。

2、局部變量, 如果是基本類型或是包裝類型,依然不能通過多線程改變其值,如果是對象,則其屬性值是線程不安全的(對象引用是局部變量,在棧內存,但是對象本身還是處於堆內存)。

3、基本類型在成員變量和局部(local)變量的時候其內存分配機制是不一樣的。
如果是成員變量,那麼不分基本類型和引用類型都是在java的堆內存裏面分配空間,而局部變量的基本類型是在棧上分配的。棧屬於線程私有的空間,局部變量的生命週期和作用域一般都很短,爲了提高gc效率,所以沒必要放在堆裏面。

Threadlocal類

1、這個類能使線程中的某個值與保存值的對象關聯起來。

2、ThreadLocal提供了get與set等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。

3、ThreadLocal對象通常用於防止對可變的單實例變量或全局變量進行共享。

4、ThreadLocal是線程Thread中屬性threadLocals的管理者。

不變性

1、即使對象中所有的域都是final類型的,這個對象也仍然是可變的,因爲在final類型的域中可以保存對可變對象的引用。

2、不可變對象滿足的條件:

  • 對象創建以後其狀態就不能修改。
  • 對象所有的域都是final類型。
  • 對象是正確創建的(創建期間this引用沒有逸出)。

final域

1、final類型的域是不能修改的,但是如果final域所引用的對象是可變的,那麼這些被引用的對象是可以修改的。

2、final域能確保初始化過程的安全,從而可以不受限制地訪問不可變對象,並在共享這些對象時無須同步。

安全發佈

不正確的發佈:正確的對象被破壞

安全發佈的常用模式

1、要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。

2、一個正確構造的對象可以通過以下方式來安全地發佈:

  • 在靜態初始化函數中初始化一個對象引用。
  • 將對象的引用保存到volatile類型的域或者AtomicReferance對象中。
  • 將對象的引用保存到某個正確構造對象的final類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

對象的組合

設計線程安全的類

1、在設計線程安全類的過程中,需要包含以下三個基本要素:

  • 找出構成對象狀態的所有變量
  • 找出約束狀態變量的不變性條件
  • 建立對象狀態的併發訪問管理策略

任務執行

在線程中執行任務

1、如果可運行的線程數量多於可用處理器的數量,那麼有些線程將閒置。

2、大量空閒的線程會佔用許多內存,給垃圾回收器帶來壓力,而且大量線程在競爭CPU資源時還將產生其他的性能開銷。

取消與關閉

任務取消

1、如果外部代碼能在某個操作正常完成之前將其置入“完成”狀態,那麼這個操作就可以稱爲可取消的。

2、Thread中斷方法:

  • inturrept():將中斷狀態置爲true。
  • isInterrupted():返回當前的中斷狀態
  • isterrupted():清除當前狀態,並返回它之前的值。

3、通常情況下,如果一個阻塞方法,如:Object.wait()、Thread.sleep()和Thread.join() 時,都會去檢查中斷狀態的值,發現中斷狀態變化時都會提前返回並響應中斷:清除中斷狀態,並拋出InterruptedException異常 。

4、該注意的是,中斷操作並不會真正的中斷一個正在運行的線程,而只是發出中斷請求,然後由程序在合適的時刻中斷自己。一般設計方法時,都需要捕獲到中斷異常後對中斷請求進行某些操作,不能完全忽視或是屏蔽中斷請求。

線程池的使用

一些概念和建議

1、如果所有正在執行任務的線程都由於等待其他處於工作隊列中的任務而阻塞,那麼會發生死鎖。這種現象被稱爲線程飢餓死鎖。

2、如果需要執行不同類別的任務,並且他們之間的行爲相差很大,那麼應該考慮使用多個線程池。

3、對於計算密集型任務,再N個處理器的系統上,當線程池的大小爲N+1時,通常能實現最優的利用率。即使當計算密集型的線程偶爾由於頁缺失或者其他原因而暫停時,這個額外的線程也能確保CPU的時鐘週期不會被浪費,

4、對於包含I/O操作或者其他阻塞操作的任務,由於線程並不會一直執行,因此線程池的規模應該更大。要正確地設置線程池的大小,你必須估算出任務的等待時間與計算時間的比值。可以設置爲2N + 1

5、線程池大小計算:N * U * (1 + W/C)

  • N:處理器個數
  • U:期望的CPU利用率
  • W/C:等待時間與計算時間的比率

配置ThreadPoolExecutor

管理隊列任務

1、只有當任務相互獨立時,爲線程池或工作隊列設置界限纔是合理的。如果任務之間存在依賴性,那麼有界的線程池可能導致飢餓死鎖問題。

飽和策略

1、如果在應用程序中需要利用安全策略來控制對某些特殊代碼庫的訪問權限,那麼可以通過Executor中的privilegedthreadFactory工廠來定製自己的線程工廠。通過這種方式創建出來的線程,將與創建privilegedthreadFactory的線程擁有相同的訪問權限、AccessControlContxt和contextClassLoader。如果不使用privilegedthreadFactory,線程池創建的線程將從再需要新線程時調用execute或submit的客戶程序中繼承訪問權限,從而導致令人困惑的安全性異常。

擴展ThreadPoolExecutor

1、ThreadPoolExecutor是可擴展的,它提供了幾個可以再子類中改寫的方法:beforeExecute、afterE小娥cute和terminated。

遞歸算法的並行化

1、如果循環中的迭代操作都是獨立的,並且不需要等待所有的迭代操作都完成再繼續執行,那麼就可以使用Executor將串行循環轉化爲並行循環。

避免活躍性危險

1、如果在持有鎖的情況下調用某個外部方法,那麼就需要警惕死鎖。

死鎖的避免與診斷

支持定時的鎖

1、還有一項技術可以檢測死鎖和從死鎖鍾回覆過來,即顯式使用Lock類中的定時tryLock功能來代替內置鎖機制。

2、當使用內置鎖時,只要沒有獲得鎖,就會永遠等待下去,而顯式鎖則可以指定一個超時時限,在等待超過該時間後tryLock會返回一個失敗信息。

線程轉儲信息來分析死鎖

1、JVM通過線程轉儲來幫助識別死鎖的發生。

2、線程轉儲包括:

  • 各個運行中的線程的棧追蹤信息,這類似於發生異常時的棧追蹤信息。
  • 加鎖信息,如每個線程持有了哪些鎖,在哪些棧幀中獲得這些鎖,以及被阻塞的線程正在等待獲取哪一個鎖。

3、在生成線程轉儲之前,JVM將在等待關係圖中通過搜索循環來找出死鎖。如果發現了一個死鎖,則獲取相應的死鎖信息。

其他活躍性危險

飢餓

1、當線程由於無法訪問它所需要的資源而不能繼續執行時,就發生了飢餓。

2、引發飢餓的最常見資源就是CPU時鐘週期。

3、你經常能發現某個程序會在一些奇怪的地方調用Thread.sleep或Thread.yield,這是因爲改程序企圖克服優先級調整問題或響應性問題,並試圖讓低優先級的線程執行更多的時間。

糟糕的響應性

活鎖

1、活鎖問題儘管不會阻塞線程,但是也不能繼續執行,因爲線程將不斷重複執行相同的操作,而且總會失敗

2、活鎖通常發生在處理事務消息的應用程序中:如果不能成功地處理某個消息,那麼消息處理機制將回滾整個事務,並將它重新放到隊列的開頭。這種消息有時候也被稱爲毒藥消息。

3、要解決這種活鎖問題,需要在重試機制中引入隨機性

性能與可伸縮性

對性能的思考

1、要想通過併發來獲得更好的性能,需要努力做好兩件事:

  • 更有效地利用現有的處理資源。
  • 在出現新的處理資源時使程序儘可能地利用這些新資源。

2、Amdahl定律:在增加計算資源的情況下,程序在理論上能夠實現最高加速比,這個值取決於程序中可並行組件與串行組件所佔的比重。

線程引入的開銷

上下文切換

1、按照經驗來看,在大多數通用的處理器中,上下文切換的開銷相當於5000~10000個時鐘週期,也就是幾微秒。

2、如果內核佔用率較高(超過10%),那麼通常表示調用活動發生的很頻繁,這很可能是由I/O或競爭鎖導致的阻塞引起的。

內存同步

1、同步操作的性能開銷包括多個方面。

2、在synchronized和volatile提供的可見性保證中可能會使用一些特殊指令,即內存柵欄(內存屏障)。內存柵欄可以刷新緩存,使緩存無效,刷新硬件的寫緩衝,以及停止執行管道。–運用了緩存一致性協議

3、在內存柵欄中,大多數操作都是不能被重排序的。

4、優化重點應該放在那些發生鎖競爭的地方。

阻塞

1、JVM在實現阻塞行爲時,可以採用自旋等待或者通過操作系統掛起被阻塞的線程。如果等待時間較短,則適合採用自旋等待方式,而如果等待時間長,則適合採用線程掛起方式。

減少鎖的競爭

1、有三種方式可以降低鎖的競爭程度:

  • 減少鎖的持有時間
  • 降低鎖的請求頻率
  • 使用帶有協調機制的獨佔鎖,這些機制允許更高的併發性。

縮小鎖的範圍(快進快出)

1、將一些與鎖無關的代碼移出同步代碼塊,尤其是那些開銷較大的操作,以及可能被阻塞操作。

減小鎖的粒度

1、另一種減小鎖的持有時間的方式是降低線程請求鎖的頻率,這可以通過鎖分解和鎖分段等技術來實現,在這些技術中將採用多個相互獨立的鎖來保護獨立的狀態變量,從而改變這些變量在之前由單個鎖來保護的情況。

鎖分段

1、將鎖分解技術進一步擴展爲對一組獨立對象上的鎖進行分解,這種情況被稱爲鎖分段。

2、ConcurrentHashMap的實現中使用了一個包含16個鎖的數組,每個鎖保護所有散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護。

3、鎖分段的一個劣勢在於:要獲取多個鎖來實現獨佔訪問將更加困難並且開銷更高。

避免熱點域

一些替代獨佔鎖的方法

1、第三種降低競爭鎖的影響的技術就是放棄使用獨佔鎖,從而有助於使用一種友好併發的方式來管理共享狀態。例如,使用併發容器、讀寫鎖、不可變對象以及原子變量。

向對象池說“不”

減少上下文切換的開銷

1、在服務器應用程序中,發生阻塞的原因之一就是在處理請求時產生各種日誌消息。

顯式鎖

Lock與ReentrantLock

1、ReentrantLock實現了Lock接口,並提供了與synchronized相同的互斥性和內存可見性。

2、在獲取ReentrantLock時,有着與進入同步代碼塊相同的內存語義,在釋放ReentrantLcok時,同樣有着與退出同步代碼塊相同的內存語義。

3、此外,與synchronized一樣,ReentrantLock還提供了可重入的加鎖語義。

輪詢鎖與定時鎖

1、可定時的和可輪詢的鎖獲取模式是由tryLock方法實現的。與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。

讀寫鎖

1、在讀取鎖和寫入鎖之間的交互可以採用多種實現方式。ReadnWriteLock中的一些可選實現包括:

  • 釋放優先:當一個寫入操作釋放寫入鎖時,並且隊列中同時存在讀線程和寫線程,那麼應該優先選擇哪個?還是按照請求順序?
  • 讀線程插隊
  • 重入性:讀取鎖和寫入鎖是否是可重入的。
  • 降級
  • 升級

2、ReentrantReadWriteLock爲讀和寫鎖都提供了可重入的語義。

3、在Java5.0中,讀取鎖的行爲更類似於一個Semaphore而不是鎖,它只維護活躍的讀線程的數量,而不考慮他們的標識。在Java6中修改了這個行爲:記錄哪些線程已經獲得了讀者鎖。

原子變量與非阻塞同步機制

Java內存模型(JMM)

Java內存模型簡介

1、Java內存模型是通過各種操作來定義的,包括對變量的讀/寫操作,監視器的加鎖和釋放操作,以及線程的啓動和合並操作。

2、JMM爲程序中所有的操作定義了一個便序關係,稱之爲Happens-Before。要想保證執行操作B的線程看到操作A的結果(無論A和B是否在同一個線程中執行),那麼在A和B之間必須滿足H-B關係。

3、如果兩個操作之間缺乏H-B關係,那麼JVM可以懟它們任意地重排序。

4、Happens-Before規則包括:

  • 程序順序規則
  • 監視器鎖規則
  • volatile變量規則
  • 線程啓動規則
  • 線程結束規則
  • 中斷規則
  • 終結器規則
  • 傳遞性
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章