(2)Java併發編程基礎篇

什麼是多線程併發編程

併發指在同一時間段內多個任務執行,而並行指單位時間內多個任務執行。同一時間段內由多個單位時間組成,所以併發的多個任務不一定在單位時間內同時執行。併發強調的是同一時間段。

爲什麼要進行多線程併發編程

多核CPU時代的到來有效解決了單核CPU的困擾,線程各自使用自己的CPU運行,減少了線程上下文切換的資源開銷,但隨着大數據時代的來臨,系統逐漸對性能和吞吐的要求提高,需要對高併發編程有這強烈的需求。

什麼是線程安全問題

指多個線程去讀寫共享資源時,出現髒數據或者不一致的情況。例如拿計數例子來說,t1時刻線程1從主存中讀取count=0,t2 時刻 將count進行計數累加得到1,此時線程2從主存讀取count=0(此處不考慮內存可見性的問題)進行計數累加得到1,t3 時刻 線程1和線程2 分別將各自計數後的count值 寫入主存。明明是兩次計數會何結果卻是1呢?其實這就是共享資源出現線程安全的問題,Java中通過synchroized 關鍵字來進行避免。

共享變量內存可見性問題

Java內存模型規定,當線程獲取共享變量時,會從主存中複製共享變量值到自己的工作內存中,線程在讀寫變量時操作的是自己的工作內存。實際線程運作的模型圖如下:
在這裏插入圖片描述
多核CPU中,每核(線程運行時)都會擁有自己獨立的 控制器 、運算器、cache。控制器包含了一組寄存器和操作控制器,運算器主要用於算術邏輯的運算,cache 爲每個核的一級緩存,是相互隔離的。在一些CPU架構中還存在二級cache用於多核之間共享。
那內存不可見指的是什麼呢?還是那計數案例來說,看以下分析

  • 線程A 首先從兩級緩存中讀取共享變量count,發現沒有命中,從主存中讀取共享變量count=0 並複製到兩級緩存中去,然後對count進行計數操作後得到count=1,線程執行完畢後,將count=1刷新到兩級緩存和主存中。(沒問題)

  • 線程B 首先從一級緩存中讀取共享變量count,發現沒有命中,進而從二級緩存中讀取共享變量count,發現命中。將count=1 複製到自己的一級緩存中,進行計數操作後得到2,線程執行完畢後,將count=2刷新到兩級緩存中和主存中。(沒問題)

  • 線程A 再次進行計數操作,首先從自己的一級緩存中獲取count,發現命中,然後獲取count=1,進行計數操作 count=2(出現問題)
    線程A進行再次計數操作時,本該獲取count的值是線程B計數之後的值,但獲取的卻是自身上次計數後的值。那麼java是通過什麼來規避這種問題的呢? 用volatile和synchronized關鍵字!

synchronized的內存語義

用synchroized包含的代碼塊,線程執行時會清除自己的工作內存,強制從主內存中讀取變量值,執行完畢後會將值刷新到主內存。但synchronized關鍵字往往會帶來線程上下文切換開銷的性能問題。

volatile關鍵字

用volatile關鍵字修飾的共享變量,線程讀取的時候會強制從主存中讀取,寫入的時候,會強制將值刷新到主存中,並不會等到線程執行完畢。

原子性操作

所謂的原子性操作是指在執行一系列的系統指令時,這些指令要麼全部執行、要麼全部不執行,不存在只執行一部分的情況。(所有的指令必須連續性的執行,中間不能有所間斷…)
如下面的代碼就是線程不安全:

public class ThreadNoSafe{
private Long value;
public Long getCount(){
	return value;
	}
public void incr(){
	 ++value
	}
}

通過java -c 命令查看該類的彙編代碼,可知++value這個自增操作由多個字節碼指令來完成 讀-改-寫這三個操作的,因此它並不滿足原子性。那如何保證上述的原子性呢?就是用synchronized關鍵字。由於synchronized關鍵字是獨佔鎖,對於沒有獲得鎖的線程就會阻塞掛起,這時就避免不了線程上下文切換的資源開銷了。那還有沒有其他的方法來實現這個原子性操作呢?答案是有的即CAS。

CAS操作

Java提供了非阻塞性volatile的關鍵字來保證內存變量的可見性,一定程度上減少了鎖資源開銷帶來的問題,但是它並不能保證原子性操作。CAS 即Compare And Swap(比較和交換),這是計算機底層硬件的一個原子性操作指令,實現比較和更新。 CAS 核心是 通過預期值與內存的值相比,如果相等則更新給定的新值,反之更新失敗。

關於CAS操作存在ABA的一個問題:
假如線程1要求修改值爲A的變量X,首先會從內存中讀取變量X(值爲A),然後通過CAS操作進行比較 將值更新爲B。至此,難道線程A更新的數據就是對的麼?未必,因爲有可能在線程1還未更新完之前,線程B也對變量X進行了CAS操作將變量從 A->B->A 了。這時線程A並不知道變量X的值是線程B已經更新過的。那Java中是如何來規避這中問題呢?使用AtomicStampedReference類,它爲每一個變量配備了一個時間戳來避免ABA問題的出現。

Java中對於CAS操作封裝的工具包都處於rt.jar中的Unsafe類中.

僞共享

爲了解決主存與CPU運行速度相差的問題,會在主存與CPU之間添加一級或二級的緩存器。如下圖:在這裏插入圖片描述
一級或二級的緩存器一般都被集成到CPU硬件當中。其中緩存器與主存進行數據交互的時候是按行進行存儲的。
在這裏插入圖片描述
當CPU訪問變量的時候,會從兩級的緩存器中尋找,如果命中則進行使用,未命中從主存中訪問變量,同時會將變量所在大小的cache行(可能包含其他的變量)緩存至兩級緩存中。多個線程同時訪問這個緩存塊與多個線程訪問一個緩存塊中只有單個變量相比性能會有所降低 ,這就是僞共享。
很顯然,僞共享這種情況java當中是會發生的,讓該如何進行避免呢?

如何避免僞共享

  1. 通過字節填充的方式來實現,保證字節填充後的大小等於緩存行的大小
  2. JDK8 提供了 @sun.misc.Contended(“tlr”) 註解用來解決僞共享的問題。例如Thread類中就有幾個屬性標註了此註解。需要注意的是此註解只能作用於java核心包,如果用戶類路徑下的類需要用此註解,則要添加JVM參數:-XX:RestrictContended 填充的默認寬度是128
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章