Java多線程編程核心技術-第2章-對象及變量的併發訪問-讀書筆記

第 2 章 對象及變量的併發訪問

本章主要內容

synchronized 對象監視器爲 Object 時的使用。
synchronized 對象監視器爲 Class 時的使用。
非線程安全是如何出現的。
關鍵字 volatile 的主要作用。
關鍵字 volation 與 synchronized 的區別及使用情況。

2.1 synchronized 同步方法

  “非線程安全”其實會在多個線程對同一個對象中的實例變量進行併發訪問時發生,產生的後果就是“髒讀”,也就是取到的數據其實是被更改過的。而“線程安全”就是以獲得的實例變量的值是經過同步處理的,不會出現髒讀的現象。

2.1.1 方法內的變量爲線程安全

  “非線程安全”問題存在於“實例變量”中,如果是方法內部的私有變量,則不存在“非線程安全”問題,所得結果也就是“線程安全”的了。

  方法中的變量不存在非線程安全問題,永遠都是線程安全的。這是方法內部的變量是私有的特性造成的。

2.1.2 實例變量非線程安全

  如果多個線程共同訪問 1 個對象中的實例變量,則有可能出現“非線程安全”問題。

  用線程訪問的對象中如果有多個實例變量,則運行的結果有可能出現交叉的情況。如果對象僅有 1 個實例變量,則有可能出現覆蓋的情況。

  在兩個線程訪問同一個對象中的同步方法時一定是線程安全的。

2.1.3 多個對象多個鎖

  兩個線程分別訪問同一個類的兩個不同實例的相同名稱的同步方法,效果卻是以異步的方式運行的。

  如果多個線程訪問多個對象,則 JVM 會創建多個鎖。

  同步的單詞爲 synchronized,異步的單詞是 asynchronized.

2.1.4 synchronized 方法與鎖對象

  調用用關鍵字 synchronized 聲明的方法一定是排隊運行的。另外需要牢牢記住“共享”這兩個字,只有共享資源的讀寫訪問才需要同步化,如果不是共享資源,那麼根本就沒有同步的必要。

  A 線程先持有 object 對象的 Lock 鎖,B 線程可以以異步的方式調用 object 對象中的非 synchronized 類型的方法。

  A 線程先持有 object 對象的 Lock 鎖,B 線程如果在這時調用 object 對象中的 synchronized 類型的方法則需要等待,也就是同步。

2.1.5 髒讀

  雖然在賦值時進行了同步,但在取值時有可能出現一些意想不到的意外,這種情況就是髒讀(dirtyRead)。發生髒讀的情況是在讀取實例變量時,此值已經被其他線程更改過了。

  髒讀是通過 synchronized 關鍵字解決的。

  當 A 線程調用 anyObject 對象加入 synchronized 關鍵字的 X 方法時,A 線程就獲得了 X 方法鎖,更準確地講,是獲得了對象的鎖,所以其他線程必須等 A 線程執行完畢纔可以調用 X 方法,但 B 線程可以隨意調用其他的非 synchronized 同步方法。

  當 A 線程調用 anyObject 對象加入 synchronized 關鍵字的 X 方法時,A 線程就獲得了 X 方法所在對象的鎖,所以其他線程必須等 A 線程執行完畢纔可以調用 X 方法,而 B 線程如果調用聲明瞭 synchronized 關鍵字的非 X 方法時,必須等 A 線程將 X 方法執行完,也就是釋放對象鎖後纔可以調用。

  髒讀一定會出現操作實例變量的情況下,這就是不同線程“爭搶”實例變量的結果。

2.1.6 synchronized 鎖重入

  關鍵字 synchronized 擁有鎖重入的功能,也就是在使用 synchronized 時,當一個線程得到一個對象鎖後,再次請求此對象鎖時是可以再次得到該對象的鎖的。這也證明在一個 synchronized 方法/塊的內部調用本類的其他 synchronized 方法/塊時,是永遠可以得到鎖的。

  “可重入鎖”的概念是:自己可以造次獲取自己的內部鎖。

  可重入鎖也支持在父子類繼承的環境中。

  當存在父子類繼承關係時,子類是完全可以通過“可重入鎖”調用父類的同步方法的。

2.1.7 出現異常,鎖自動釋放

  當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。

2.1.8 同步不具有繼承性

  同步不可以繼承。

2.2 synchronized 同步語句塊

  用關鍵字 synchronized 聲明方法在某些情況下是有弊端的,比如 A 線程調用同步方法執行一個長時間的任務,那麼 B 線程則必須等待比較長時間,這樣的搶礦下可以使用 synchronized 同步語句塊來解決。

2.2.1 synchronized 方法的弊端

  弊端就是 A 線程調用同步方法執行一個長時間的任務,那麼 B 線程則必須等待比較長時間。解決這樣的問題可以使用 synchronized 同步塊。

2.2.2 synchronized 同步代碼塊的使用

  當兩個併發線程訪問同一個對象 object 中的 synchronized(this) 同步代碼塊中,一段時間內只能有一個線程被執行,另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。

2.2.3 用同步代碼塊解決同步方法的弊端

  當一個線程訪問 object 的一個 synchronized 同步代碼塊時,另一個線程仍然可以訪問該 object 對象中的非 synchronized(this) 同步代碼塊。

2.2.4 一半異步,一半同步

  不在 synchronized 塊中就是異步執行,在 synchronized 塊中就是同步執行。

2.2.5 synchronized 代碼塊間的同步性

  在使用同步 synchronized(this) 代碼塊時需要注意的是,當一個線程訪問 object 的一個 synchronized(this) 同步代碼塊時,其他線程對同一個 object 中所有其他 synchronized(this) 同步代碼塊的訪問將被阻塞,這說明 synchronized 使用的 “對象監視器” 是一個。

2.2.6 驗證同步 synchronized(this) 代碼塊是鎖定當前對象的

  和 synchronized 方法一樣,synchronized(this) 代碼塊也是鎖定當前對象的。

2.2.7 將任意對象作爲對象監視器

  多個線程調用同一個對象中的不同名稱的 synchronized 同步方法或 synchronized(this) 同步代碼塊時,調用的效果就是按順序執行,也就是同步的,阻塞的。

  這說明 synchronized 同步方法或 synchronized(this) 同步代碼塊分別有兩種作用。

  (1)synchronized 同步方法
  1)對其他 synchronized 同步方法或 synchronized(this) 同步代碼塊調用呈阻塞狀態。
  2)同一時間只有一個線程可以執行 synchronized 同步方法中的代碼。

  (2)synchronized(this) 同步代碼塊
  1)對其他 synchronized 同步方法或 synchronized(this) 同步代碼塊調用呈阻塞狀態。
  2)同意時間只有一個線程可以執行 synchronized(this) 同步代碼塊中的代碼。

  使用 synchronized(this) 格式來同步代碼塊,其實 Java 還支持對 “ 任意對象 ” 作爲 “ 對象監視器 ” 來實現同步的功能。這個 “ 任意對象 ” 大多數是實例變量及方法的參數,使用格式爲 synchronized(非 this 對象)。

  根據前面對 synchronized(this) 同步代碼塊的作用總結可知,synchronized(非 this 對象) 格式的作用只有 1 種:synchronized(非 this 對象 x)同步代碼塊。

  1)在多個線程持有 “對象監視器” 爲同一個對象的前提下,同一時間只有一個線程可以執行 synchronized(非 this 對象 x)同步代碼塊中的代碼。

  2)當持有 “ 對象監視器 ” 爲同一個對象的前提下,同一時間只有一個線程可以執行 synchronized( 非 this 對象 x )同步代碼塊中的代碼。

  鎖非 this 對象具有一定的優點:如果在一個類中有很多個 synchronized 方法,這時雖然能實現同步,但會受到阻塞,所以影響運行效率;但如果使用同步代碼塊鎖非 this 對象,則 synchronized(非 this) 代碼塊中的程序與同步方法是異步的,不與其他鎖 this 同步方法爭搶 this 鎖,則可大大提高運行效率。

  使用 “ synchronized( 非 this 對象 x ) 同步代碼塊”格式進行同步操作時,對象監視器必須是同一個對象。如果不是同一個對象監視器,運行的結果就是異步調用了,就會交叉運行。

  同步代碼塊放在非同步 synchronized 方法中進行聲明,並不能保證調用方法的線程的執行同步 / 順序性,也就是線程調用方法的順序是無須的,雖然在同步塊中執行的順序是同步的,這樣極易出現 “ 髒讀 ” 問題。使用 “ synchronized( 非 this 對象 x ) 同步代碼塊 ” 格式也可以解決 “ 髒讀 ” 問題。

2.2.8 細化驗證 3 個結論

  “ synchronized ( 非 this 對象 x ) ” 格式的寫法是將 x 對象本身作爲 “ 對象監視器 ”,這樣就可以得出以下 3 個結論:

  1)當多個線程同時執行 synchronized(x){} 同步代碼塊時呈同步效果。

  2)當其他咸亨執行 x 對象中 synchronized 同步方法時呈同步效果。

  3)當其他線程執行 x 對象方法裏面的 synchronized(this) 代碼塊時也呈現同步效果。

  但需要注意:如果其他線程調用不加 synchronized 關鍵字的方法時,還是異步調用。

2.2.9 靜態同步 synchronized 方法與 synchronized(class) 代碼塊

  關鍵字 synchronized 還可以應用在 static 靜態方法上,如果這樣寫,那就是對當前的 *.java 文件對應的 Class 類進行持鎖。

  給靜態方法加關鍵字 synchronized 和將 synchronized 關鍵字加到非 static 方法上使用的效果是一樣的,都是同步的效果。其實還是有本質上的不同的,synchronized 關鍵字加到 static 靜態方法上是給 Class 類上鎖,而 synchronized 關鍵字加到非 static 靜態方法上是給對象上鎖。而 Class 鎖可以對類的所有對象實例起作用。

  同步 synchronized(class) 代碼塊的作用其實和 synchronized static 方法的作用一樣。

2.2.10 數據類型 String 的常量池特性

  在 JVM 中具有 String 常量池緩存的功能。

  將 synchronized(string) 同步塊與 String 聯合使用時,要注意常量池帶來的一些例外。

  如果 String 的兩個值是相同的,兩個線程持有相同的鎖,就會造成一個線程不行執行,這就是 String 常量池所帶來的問題。因此在大多數的情況下,同步 synchronized 代碼塊都不使用 String 作爲鎖對象,而改用其他,比如 new Object() 實例化一個 Object 對象,但它並不放入緩存中。

2.2.11 同步 synchronized 方法無限等待與解決

  同步方法容易造成死循環。可以使用同步塊來解決問題。

2.2.12 多線程的死鎖

  Java 線程死鎖是一個經典的多線程問題,因爲不同的線程都在等待根本不可能被釋放的鎖,從而導致所有的任務都無法繼續完成。在多線程技術中。“死鎖”是必須避免的,因爲這會造成線程的“假死”。

  可以使用 JDK 自帶的工具來監測是否有死鎖的現象。jps 命令、jstack 命令。

  死鎖是程序設計的 Bug,在設計程序時就要避免雙方互相持有對方的鎖的情況。

2.2.13 內置類與靜態內置類

  關鍵字 synchronized 的知識點還涉及內置類的使用。

2.2.14 內置類與同步:實驗 1

  在內置類中有兩個同步方法,但使用的確實不同的鎖,打印的結果也是異步的。

2.2.15 內置類與同步:實驗 2

  同步代碼塊 synchronized(class2) 對 class2 上鎖後,其他線程只能以同步的方式調用 class2 中的靜態同步方法。

2.2.16 鎖對象的改變

  在將任何數據類型作爲同步鎖時,需要注意的是,是否有多個線程同時持有鎖隨想,如果同時持有相同的鎖對象,則這些線程之間就是同步的;如果分別獲得鎖對象,這些線程之間就是異步的。

  只要對象不變,即使對象的屬性被改變,運行的結果還是同步。

2.3 volatile 關鍵字

  關鍵字 volatile 的主要作用是使變量在多個線程間可見。

2.3.1 關鍵字 volatile 與死循環

  如果不是在多繼承的情況下,使用繼承 Thread 類和實現 Runnable 接口在取得程序運行的結果上並沒有什麼太大的區別。如果一旦出現“多繼承”的情況,則用實現 Runnable 接口的方式來處理多線程的問題就是很有必要的。

2.3.2 解決同步死循環

  在方法中處理 while() 循環,導致程序不能繼續執行後面的代碼,線程就無法停止下來。解決的辦法是用多線程技術。

  將 while() 循環的執行放入線程中,然後出現死循環,解決的辦法是使用 volatile 關鍵字。

  關鍵字 volatile 的作用是強制從公共堆棧中取得變量的值,而不是從線程私有數據棧中取得變量的值。

2.3.3 解決異步死循環

  通過使用 volatile 關鍵字,強制的從公共內存中讀取變量的值。

  使用 volatile 關鍵字增加了實例變量在多個線程之間的可見性。但 volatile 關鍵字最致命的缺點是不支持原子性。

  下面將關鍵字 synchronized 和 volatile 進行一下比較:

  1)關鍵字 volatile 是線程同步的輕量級實現,所以 volatile 性能肯定比 synchronized 要好,並且 volatile 只能修飾於變量,而 synchronized 可以修飾方法,以及代碼塊。隨着 SDK 新版本的發佈,synchronized 關鍵字在執行效率上得到大提升,在開發中使用 synchronized 關鍵字的比率還是比較大的。

  2)多線程訪問 volatile 不會發生阻塞,而 synchronized 會出現阻塞。

  3)volatile 能保證數據的可見性,但不能保證原子性;而 synchronized 可以保證原子性,也可以間接保證可見性,因爲它會將私有內存和公共內存中的數據做同步。

  4)再次重申一下,關鍵字 volatile 解決的是變量在多個線程之間的可見性;而 synchronized 關鍵字解決的是多個線程之間訪問資源的同步性。

  線程安全包含原子性和可見性兩個方面,Java 的同步機制都是圍繞這兩個方面來確保線程安全的。

2.3.4 volatile 非原子的特性

  關鍵字 volatile 雖然增加了實例變量在多個線程之間的可見性,但它不具備同步性,那麼也就不具備原子性。

  關鍵字 volatile 主要使用的場合是在多個線程中可以感知實例變量被更改了,並且可以獲得最新的值使用,也就是用多線程讀取共享變量時可以獲得最新值使用。

  關鍵字 volatile 提示線程每次從共享內存中讀取變量,而不是從私有內存中讀取,這樣就保證了同步數據的可見性。

  表達式 i++ 的操作步驟分解如下:

  1)從內存中取出 i 的值;

  2)計算 i 的值;

  3)將 i 的值寫到內存中。

  假如在第 2 步計算值的時候,另外一個線程也修改 i 的值,那麼這個時候就會出現髒數據。解決的辦法其實就是使用 synchronized 關鍵字。所以說 volatile 本身並不處理數據的原子性,而是強制對數據的讀寫及時影響到主內存的。

  變量在內存中工作的過程如下圖:

  由此,可以得出一下結論:

  1)read 和 load 階段:從主存複製變量到當前線程工作內存;

  2)use 和 assign 階段:執行代碼,改變共享變量值;

  3)store 和 write 階段:用工作內存數據刷新主存對應變量的值。

  在多線程環境中,use 和 assign 時多次出現的,但這一操作並不是原子性,也就是在 read 和 load 之後,如果主內存 count 變量發生修改之後,線程工作內存中的值由於已經加載,不會產生對應的變化,也就是私有內存和共有內存中的變量不同步,所以計算出來的結果會和預期不一樣,也就出現了非線程安全問題。

  對於用 volatile 修飾的變量,JVM 虛擬機只是保證從主內存加載到線程工作內存的值是最新的。也就是說,volatile 關鍵字解決的是變量讀時的可見性問題,但無法保證原子性,對於多個線程訪問同一個實例變量還是需要加鎖同步。

2.3.5 使用原子類進行 i++ 操作

  除了在 i++ 操作時使用 synchronized 關鍵字實現同步外,還可以使用 AtomicInteger 原子類進行實現。

  原子操作是不能分割的整體,沒有其他線程能夠中斷或檢查正在原子操作中的變量。一個原子(atomic)類型就是一個原子操作可用的類型,它可以在沒有鎖的情況下做到線程安全(thread-safe)。

2.3.6 原子類也並不完全安全

  原子類在具有有邏輯性的情況下輸出結果也具有隨機性。

  出現這種情況是因爲方法是原子的,但方法和方法之間的調用卻不是原子的。解決這樣的問題必須要用同步。

2.3.7 synchronized 代碼塊有 volatitle 同步的功能

  關鍵字 synchronized 可以使多個線程訪問同一個資源具有同步性,而且它還具有將線程工作內存中的私有變量與公共內存中的變量同步的功能。

  關鍵字 synchronized 可以保證在同一時刻,只有一個線程可以執行某一個方法或某一個代碼塊。它包含兩個特徵:互斥性和可見性。同步 synchronized 不僅可以解決一個線程看到對象處於不一致的狀態,還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護之前所有的修改效果。

2.4 本章總結

  學習完多線程同步後就可以有效控制線程間處理數據的順序性,及對處理後的數據進行有效值的保證,更好地對線程執行結果有正確的預期。

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