1. 程式人生 > >關於記憶體安全,執行緒安全,死鎖(上)

關於記憶體安全,執行緒安全,死鎖(上)

 

1.基本概念

 

這三樣東西知識點很多,接觸多執行緒程式設計必然接觸到,專門理一理

也算開個坑,很多細節沒有細緻解釋,後面遇到需要深挖

 

執行緒安全雖然處處接觸到記憶體,但跟記憶體安全還不是一回事,記憶體安全可以被定義為:不訪問任何未定義的記憶體。如:避免緩衝區溢位,避免引用未初始化等。

可以說記憶體安全涉及到記憶體的分配回收等偏底層操作。

 

執行緒安全被定義為:多個執行緒訪問類時,無論採取何種排程方式,主調程式碼中也不需要額外的同步和協同,都能表現出正確的行為。這裡的“類”應該被叫成共享資料,同樣是對記憶體進行操作,執行緒安全考慮的是其中包含資料的安全性,而非記憶體地址本身的問題。即考慮的是買下來的房子有沒有進賊,而非糾結這塊地是不是我的。

 

死鎖,定義一般是下在程序上的:死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去在網上找到的解釋大多不分程序執行緒,甚至有寫著寫著就改口的情況。我們知道,程序是計算機分配資源的基本單位,而執行緒是系統獨立排程和分派的基本單位,執行緒不佔有獨立的資源,但上鎖的共享資料之於執行緒一樣能帶來執行緒的死鎖。

 

 

2.執行緒安全

 

執行緒安全的判定圍繞著三個性:原子性,可見性,順序性。

原子性指一件事要麼幹完,要麼全不幹;可見性要求一個執行緒對資料的修改是透明的,即要求大家都能知道;順序性,由《深入理解java虛擬機器》中對

JSR提到的happens-before八條規則做了說明,可以看看,由這些規則可以匯出的次序才是有序的。

在此理清執行緒安全幾個常見關鍵字:synchronized,volatile,wait,notify,lock。

其實只有synchronized是真正意義上的java關鍵詞,這裡單純用關鍵詞的表意。

 

Synchronized:

首先是synchronized關鍵字作用域,總結來說,都是鎖物件的:修飾例項方法時,鎖住該例項物件;修飾靜態方法時,鎖住當前類物件;修飾程式碼塊時,鎖住給定物件;synchronized static 雖然叫做類鎖,但光鎖類是沒有意義的,jvm只給物件在堆裡開闢記憶體空間,類鎖鎖住了該類的所有例項物件,還是物件。

考慮到synchronized的實現原理,私以為用鎖來比喻有點不夠恰當:物件在記憶體中佈局中有一塊叫物件頭,其中有一條記錄叫重量級鎖,即synchronized鎖,標誌位是10,指標指向一個叫monitor的東西,姑且叫做保安,保安的資料結構如下:

ObjectMonitor() {

_header = NULL;

_count = 0; //記錄個數

_waiters = 0,

_recursions = 0;

_object = NULL;

_owner = NULL;

_WaitSet = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet

_WaitSetLock = 0 ;

_Responsible = NULL ;

_succ = NULL ;

_cxq = NULL ;

FreeNext = NULL ;

_EntryList = NULL ; //處於等待鎖block狀態的執行緒,會被加入到該列表

_SpinFreq = 0 ;

_SpinClock = 0 ;

OwnerIsThread = 0 ;

}

保安會記錄來訪的人,獲取許可權的人,來訪沒能獲取許可權的人,現在有沒有人在訪問等,jvm給每個物件(房子)都提供一個這樣的保安。

來看看synchronized如何利用monitor機制來確保對物件的安全訪問:

通過反編譯程式碼後的結果可以看出來,synchronized是在程式碼塊中資料操作前加上“monitorenter”指令,結束時加上“monitorexit”指令,入指令會訪問保安,保安計數器為0時可獲取持有權並將計數器加一,如果被佔有則進入阻塞狀態,出指令會到保安處登記,將計數器置0並退出。

因為保安系統是要依賴作業系統層的mutex lock來實現,而每次要在使用者態和核心態直接轉化,故時間成本較高,java後來對synchronized做了優化,引入了輕量鎖,偏向鎖。

 

Volatile:

什麼叫可見性,是共享變數的改變能及時一致的更新。我們來硬體層面看看不一致更新導致的問題:

計算機在找資料時找目標並非完全隨機的,有時間區域性性和空間區域性性,所以在cpu中設有快取記憶體,速度高的代價就是價格高,所以普遍容量較小。快取存的就是最近用到的資料,不同快取都從主存中讀取同一條資料,可能A執行緒處理完了存回主存,B還在用它快取的,可能B處理完了存回快取,但未更新到主存,A從主存提取的資料就不是最新的了。這類問題都可以被歸結為資料狀態不透明,volatile就是用於解決這一問題。

聲明瞭Volatile的變數寫操作時,會向處理器傳送lock字首的指令。在P6之前比如奔騰處理器中,處理器在申明瞭lock的指令執行期間,會將匯流排鎖起,其他CPU無法訪問匯流排就無法訪問記憶體。之後的處理器中,快取一致協議讓每個處理器嗅探其他處理器和記憶體,包括自己的快取,嗅探到其他處理器打算改寫入一個記憶體地址,而這個地址為共享狀態,則該處理器將自己快取中的該資料置為無效,並在下次訪問時直接從記憶體中提取。

* 一個很容易混淆的問題:volatile申明的資料可以保證透明性,但不能保證原子性。

很多解釋都解釋的不是很清楚:比如說自增操作分為三步,分成volatile不能保證三步的原子性,沒有寫操作不會導致記憶體值的改變等。我們詳細看一下執行緒1,2對資料的操作:1從記憶體讀取資料inc,放在自己處理器快取中,2讀取後並加1並寫入記憶體,這時確實處理器能保證資料的透明性,1執行的處理器將自己快取中的inc作廢,但是這是有範圍的,執行緒1讀取的inc會在主存中開闢一塊記憶體作為副本並下一步用副本資料進行操作,所以從原理上解釋應該成透明性的作用域是有限的,快取一致不代表所有資料都一致。解決這個問題可用狀態標記,double check保證。

 

wait,notify:

用法不做表述,需要理解的是,wait和notify的作用物件是執行緒本身,跟synchronized作用於物件區分開來。Wait要求執行緒在已經獲取鎖的狀態下放棄資源,進入阻塞狀態,等待notify將其喚醒。所以對共享資料的鎖比作房子的安保制度,wait和notify操作的是對訪問房子的人的行為進行規劃。在理清這一概念後瞭解其工作流程就變得簡單了。

 

Lock:

lock要和synchronized放在一起說明,synchronized的缺陷有:鎖釋放不靈活,容易被死鎖;讀取操作這一可以並行的操作也被禁止;無法瞭解控制加減鎖。

Lock本質上作為一個介面,提供了一些方法解決:

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的,unLock()方法是用來釋放鎖的。

由於一些特質:

Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行;

通過Lock可以知道有沒有成功獲取鎖;

Lock可以提高多個執行緒進行讀操作的效率;

在資源競爭非常激烈的時候,lock的效率遠遠高出synchronized

 

 

後續會總結死鎖和多執行緒框架Executor,為多執行緒程式設計打下牢固基礎。