JAVA基礎知識系列---程式、執行緒安全

glmapper發表於2017-10-08

1.1 臨界區

保證在某一時刻只有一個執行緒能訪問資料的簡便方法,在任意時刻只允許一個執行緒對資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼在有一個執行緒進入後,其他所有試圖訪問臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共享資源的目的

1.2 互斥量

互斥量和臨界區很相似,只能擁有互斥物件的執行緒才能具有訪問資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下次共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後可以訪問資源。互斥量比臨界區複雜,因為使用互斥不僅僅能夠在同一應用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享。

1.3 管程/訊號量

管程和訊號量是同一個概念。指一個互斥獨佔鎖定的物件或稱為互斥體。在給定的時間,僅有一個執行緒可以獲得管程。當一個執行緒需要鎖定,他必須進入管程。所有其他的試圖進入已經鎖定的管程的執行緒必須掛起直到第一個執行緒退出管程。這些其他的執行緒被稱為等待執行緒。一個擁有管程的執行緒如果願意的話可以再次進入相同的管程(可重入性)

1.4 CAS操作

CAS操作(compare and swap)CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它執行緒去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它執行緒去修改它,悲觀鎖效率很低。下面來看一下AtomicInteger是如何利用CAS實現原子性操作的。

1.5 重排序

編譯器和處理器為了提高效能,而在程式執行時會對程式進行重排序。他的出現是為了提高程式的併發度。從而提高效能;但是對於多執行緒程式,重排序可能會導致程式執行的結果不是我們需要的結果,重排序分為編譯器和處理器倆個方面。而處理器重排序包括指令級重排序和記憶體重排序。

小節

在java中,所有的變數(例項欄位,靜態欄位,構成陣列的元素,不包括區域性變數和方法引數)都儲存在主記憶體中,內個執行緒都有自己的工作記憶體,執行緒的工作記憶體儲存被執行緒使用到的變數的主記憶體副本拷貝。執行緒對變數的所有操作都必須在工作記憶體中進行,為不能直接讀寫主記憶體的變數。不同執行緒之間也不恩能夠直接訪問對方工作記憶體中的變數,執行緒間比變數值的傳遞通過主記憶體來完成。

JAVA中執行緒安全相關關鍵字及類

主要包括:synchronized,Volitile,ThreadLocal,Lock,Condition

2.1 Volitile

作用:

1)保證了心智慧立即儲存到主記憶體才,每次使用前立即從主記憶體中重新整理

2)禁止指令重排序優化

Volitile關鍵字不能保證在多執行緒環境下對共享資料的操作的正確性,可以使用在自己狀態改變之後需要立即通知所有執行緒的情況下,只保證可見性,不保證原子性。即通過重新整理變數值確保可見性。

Java中synchronized和final也能保證可見性

synchronized:同步快通過變數鎖定前必須清空工作記憶體中的變數值,重新從主記憶體中讀取變數值,解鎖前必須把變數值同步回主記憶體來確保可見性。

final:被final修飾的欄位在構造器中一旦被初始化完成,並且構造器沒有把this引用傳遞進去,那麼在其他執行緒中就能看見final欄位的值,無需同步就可以被其他執行緒正確訪問。

2.2 synchronized

把程式碼塊宣告為synchronized,有倆個作用,通常是指改程式碼具有原子性和可見性。如果沒有同步機制提供的這種可見性,執行緒看到的共享比那裡可能是修改前的值或不一致的值,這將引發許多嚴重問題。

原理:當物件獲取鎖是,他首先是自己的快取記憶體無效,這樣就可以保證直接從主記憶體中裝入變數,同樣在物件釋放鎖之前,他會重新整理其快取記憶體,強制使已做的任何更改都出現在主記憶體中,這樣會保證在同一個鎖上同步的倆個執行緒看到在synchronized塊內修改的變數的相同值。

synchronized釋放由JVM自己管理。

存在的問題:

1)無法中斷一個正在等待獲得鎖的執行緒

2)無法通過投票得到鎖,如果不想等待下去,也就沒法得到鎖

3)同步還需要鎖的釋放只能在與獲得鎖所在的堆疊幀相同的堆疊中進行,多數情況下,這沒問題(而且與一場處理互動的很好),但是,確實存在一些非塊結構的鎖定更適合情況。

2.3 Lock

Lock是有JAVA編寫而成的,在java這個層面是無關JVM實現的。包括:ReentrantLock,ReadWriteLock。其本質都依賴於AbstractQueueSynchronized類。Lock提供了很多鎖的方式,嘗試鎖,中斷鎖等。釋放鎖的過程由JAVA開發人員自己管理。

就效能而言,對於資源衝突不多的情況下synchronized更加合理,但如果資源訪問衝突多的情況下,synchronized的效能會快速下降,而Lock可以保持平衡。

2.4 condition

Condition將Object監視器方法(wait,notify,notifyall)分解成截然不同的物件,以便通過這些物件與任意Lock實現組合使用,為每個物件提供多個等待set(wait-set),,其中Lock替代了synchronized方法和語句的使用,condition替代了Object監視器方法的使用。Condition例項實質上被你繫結到一個鎖上。要為特定Lock例項獲得Condition例項,請使用其newCondition()方法。

2.5 ThreadLock

執行緒區域性變數。

變數是同一個,但是每個執行緒都使用同一個初始值,也就是使用同一個變數的一個新的副本,這種情況下TreadLocal就非常有用。

應用場景:當很多執行緒需要多次使用同一個物件,並且需要該物件具有相同初始值的時候,最適合使用TreadLocal。

事實上,從本質上講,就是每個執行緒都維持一個MAP,而這個map的key就是TreadLocal,而值就是我們set的那個值,每次執行緒在get的時候,都從自己的變數中取值,既然從自己的變數中取值,那就肯定不存線上程安全的問題。總體來講,TreadLocal這個變數的狀態根本沒有發生變化。它僅僅是充當了一個key的角色,另外提供給每一個執行緒一個初始值。如果允許的話,我們自己就能實現一個這樣的功能,只不過恰好JDK就已經幫助我們做了這個事情。

使用TreadLocal維護變數時,TreadLocal為每個使用該變數的執行緒提供獨立地變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會英語其他執行緒所對應的副本。從執行緒的角度看,目標變數物件是執行緒的本地變數,這也是類名中Local所需要表達的意思。

TreadLocal的四個方法:

void set(Object val),設定當前執行緒的執行緒區域性變數的值

Object get()返回當前執行緒所對用的執行緒區域性變數。

void remove() 將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用,執行緒結束後,區域性變數自動被GC

Object initValue() 返回該執行緒區域性變數的初始值,使用protected修飾,顯然是為了讓子類覆蓋而設計的。

執行緒安全的實現方式

3.1 互斥同步

在多執行緒訪問的時候,保證同一時間只有一條執行緒使用。

臨界區,互斥量,管程都是同步的一種手段。

java中最基本的互斥同步手段是synchronized,編譯之後會形成monitorenter和monitorexit這倆個位元組碼指令,這倆個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件,還有一個鎖的計數器,來記錄加鎖的次數,加鎖幾次就要同樣解鎖幾次才能恢復到無鎖狀態。

java的執行緒是對映到作業系統的原生執行緒之上的,不管阻塞還是喚醒都需要作業系統的幫助完成,都需要從使用者態轉換到核心態,這是很耗費時間的,是java語言中的一個重量級的操作,雖然虛擬機器本身會做一點優化的操作,比如通知作業系統阻塞之前會加一段自旋等待的過程,避免頻繁切換到核心態。

3.2 非阻塞同步

互斥和同步最主要的問題就是阻塞和喚醒所帶來的效能的問題,所以這通常叫阻塞同步(悲觀的併發策略).隨著硬體指令集的發展,我們有另外的選擇:基於衝突檢測的樂觀併發策略,通俗講就是先操作,如果沒有其他執行緒爭用共享的資料,操作就成功,如果有,則進行其他的補償(最常見的就是不斷的重試)。這種樂觀的併發策略許多實現都不需要把執行緒先掛起,這種同步操作被稱為非阻塞同步。

3.3 無同步

部分程式碼天生就是執行緒安全的,不需要同步。

1)可重入程式碼:純程式碼,具有不依賴儲存在堆上的資料和公用的系統資源,用到的狀態量都由引數中傳入,不呼叫非可重入的方法等特徵,它的返回結果是可以預測的。

2)執行緒本地儲存:把共享資料的可見性範圍限制在同一個執行緒之內,這樣就無需同步也能保證執行緒之間不出現資料爭用問題。可以通過java.lang.TreadLocal類來實現執行緒本地儲存的功能。

相關文章