1. 程式人生 > >多執行緒學習總結(二)

多執行緒學習總結(二)

一、多執行緒帶來的問題

(一)活躍性問題

  1. 死鎖:兩個執行緒相互等待對方釋放資源
  2. 飢餓: 多執行緒併發時優先順序低的執行緒永遠得不到執行;執行緒被永久阻塞在一個等待進入同步塊的狀態;等待的執行緒永遠不被喚醒
  3. 活鎖:活鎖指的是任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試—失敗—嘗試—失敗的過程。處於活鎖的實體是在不斷的改變狀態,活鎖有可能自行解開。

如何避免飢餓問題:

  • 對於優先順序引發的飢餓問題,用setPriority設定執行緒的優先順序。 
  • 對於永久阻塞引發的飢餓問題,用鎖來代替synchronized。

(二)執行緒安全性問題

1.什麼是執行緒安全問題?

從某個執行緒開始訪問到訪問結束的整個過程,如果有一個訪問物件被其他執行緒修改,那麼對於當前執行緒而言就發生了執行緒安全問題;如果在整個訪問過程中,無一物件被其他執行緒修改,就是執行緒安全的。

2.執行緒安全問題產生的根本原因?

  • 首先是多執行緒環境,即同時存在有多個操作者,單執行緒環境不存線上程安全問題。在單執行緒環境下,任何操作包括修改操作都是操作者自己發出的,操作者發出操作時不僅有明確的目的,而且意識到操作的影響。
  • 多個操作者(執行緒)必須操作同一個物件,只有多個操作者同時操作一個物件,行為的影響才能立即傳遞到其他操作者。
  • 多個操作者(執行緒)對同一物件的操作必須包含修改操作,共同讀取不存線上程安全問題,因為物件不被修改,未發生變化,不能產生影響。

綜上可知,執行緒安全問題產生的根本原因是共享資料存在被併發修改的可能,即一個執行緒讀取時,允許另一個執行緒修改。

3.執行緒安全問題解決思路?

根據執行緒安全問題產生的條件,解決執行緒安全問題的思路是消除產生執行緒安全問題的環境:

  • 消除共享資料:成員變數與靜態變數多執行緒共享,將這些全域性變數轉化為區域性變數,區域性變數存放在棧,執行緒間不共享,就不存線上程安全問題產生的環境了。消除共享資料的不足:如果需要一個物件採集各個執行緒的資訊,或者線上程間傳遞資訊,消除了共享物件就無法實現此目的。
  • 使用執行緒同步機制:給讀寫操作同時加鎖,使得同時只有一個執行緒可以訪問共享資料。如果單單給寫操作加鎖,同時只有一個執行緒可以執行寫操作,而讀操作不受限制,允許多執行緒併發讀取,這時就可能出現不可重複讀的情況,如一個持續時間比較長的讀執行緒,相隔較長時間讀取陣列同一索引位置的資料,正好在這兩次讀取的時間內,一個執行緒修改了該索引處的資料,造成該執行緒從同一索引處前後讀取的資料不一致。是同時給讀寫加鎖,還是隻給寫加鎖,根據具體需求而定。同步機制的缺點是降低了程式的吞吐量。
  • 建立副本:使用ThreadLocal為每一個執行緒建立一個變數的副本,各個執行緒間獨立操作,互不影響。該方式本質上是消除共享資料思想的一種實現。

執行緒的優先順序:

Thread類有三個優先順序靜態常量:MAX_PRIORITY為10,為執行緒最高優先順序;MIN_PRIORITY取值為1,為執行緒最低優先順序;NORM_PRIORITY取值為5,為執行緒中間位置的優先順序。預設情況下,執行緒的優先順序為NORM_PRIORITY;使用Thread的setPriority()方法來設定執行緒的優先順序,設定的值越大(1~10),優先順序越高,執行的機率越大。

(二)synchronized

  • 每個java物件都可以用做一個實現同步的鎖,這些鎖成為內建鎖。執行緒進入同步程式碼塊或方法的時候會自動獲得該鎖,在退出同步程式碼塊或方法時會釋放該鎖。獲得內建鎖的唯一途徑就是進入這個鎖的保護的同步程式碼塊或方法;

  • java內建鎖是一個互斥鎖,這就是意味著最多隻有一個執行緒能夠獲得該鎖,當執行緒A嘗試去獲得執行緒B持有的內建鎖時,執行緒A必須等待或者阻塞,直到執行緒B釋放這個鎖,如果B執行緒不釋放這個鎖,那麼A執行緒將永遠等待下去;

  • synchronized修飾普通方法,預設內建鎖就是當前類的例項;

  • synchronized修飾靜態方法,預設內建鎖就是當前類的Class位元組碼物件;

  • synchronized修飾程式碼塊,鎖可以為一個例項物件或者一個類的位元組碼檔案;

  • 任何物件都可以作為鎖,鎖資訊存在於物件頭(Mark Word)中。

(三)volatile:

1.volatile的作用

  • 保證執行緒間變數的可見性(不保證原子性):執行緒對變數進行修改之後,要立刻回寫到主記憶體;執行緒對變數讀取的時候,要從主記憶體中讀,而不是快取。

Java為了保證其平臺性,使Java應用程式與作業系統記憶體模型隔離開,需要定義自己的記憶體模型。在Java記憶體模型中,記憶體分為主記憶體和工作記憶體兩個部分,其中主記憶體是所有執行緒所共享的,而工作記憶體則是每個執行緒分配一份,各執行緒的工作記憶體間彼此獨立、互不可見,線上程啟動的時候,虛擬機器為每個記憶體分配一塊工作記憶體,不僅包含了執行緒內部定義的區域性變數,也包含了執行緒所需要使用的共享變數(非執行緒內構造的物件)的副本,即為了提高執行效率,讀取副本比直接讀取主記憶體更快(這裡可以簡單地將主記憶體理解為虛擬機器中的堆,而工作記憶體理解為棧(或稱為虛擬機器棧),棧是連續的小空間、順序入棧出棧,而堆是不連續的大空間,所以在棧中定址的速度比堆要快很多)。工作記憶體與主記憶體之間的資料交換通過主記憶體來進行,如下圖:QQ截圖20131228132842

同時,Java記憶體模型還定義了一系列工作記憶體和主記憶體之間互動的操作及操作之間的順序的規則(這規則比較多也比較複雜,參見《深入理解Java虛擬機器-JVM高階特性與最佳實踐》第12章12.3.2部分),這裡只談和volatile有關的部分。對於共享普通變數來說,約定了變數在工作記憶體中發生變化了之後,必須要回寫到工作記憶體(遲早要回寫但並非馬上回寫),但對於volatile變數則要求工作記憶體中發生變化之後,必須馬上回寫到工作記憶體,而執行緒讀取volatile變數的時候,必須馬上到工作記憶體中去取最新值而不是讀取本地工作記憶體的副本,此規則保證了前面所說的“當執行緒A對變數X進行了修改後,線上程A後面執行的其他執行緒能看到變數X的變動”。

(連結:https://www.cnblogs.com/xll1025/p/6486170.html

  • 禁止指令重排序

2. volatile的適用場景

  • volatile是在synchronized效能低下的時候提出的。如今synchronized的效率已經大幅提升,所以volatile存在的意義不大。
  • 如今非volatile的共享變數,在訪問不是超級頻繁的情況下,已經和volatile修飾的變數有同樣的效果了。
  • volatile不能保證原子性,這點是大家沒太搞清楚的,所以很容易出錯。
  • volatile可以禁止重排序。

 

 

 

 

 

(注:只做個人學習總結,如有侵權請聯絡我,有的資料是網上獲取的。。。。。)