關於記憶體安全,執行緒安全,死鎖(上)
1.基本概念
這三樣東西知識點很多,接觸多執行緒程式設計必然接觸到,專門理一理
也算開個坑,很多細節沒有細緻解釋,後面遇到需要深挖
執行緒安全雖然處處接觸到記憶體,但跟記憶體安全還不是一回事,記憶體安全可以被定義為:不訪問任何未定義的記憶體。如:避免緩衝區溢位,避免引用未初始化等。
可以說記憶體安全涉及到記憶體的分配回收等偏底層操作。
執行緒安全被定義為:多個執行緒訪問類時,無論採取何種排程方式,主調程式碼中也不需要額外的同步和協同,都能表現出正確的行為。這裡的“類”應該被叫成共享資料,同樣是對記憶體進行操作,執行緒安全考慮的是其中包含資料的安全性,而非記憶體地址本身的問題。即考慮的是買下來的房子有沒有進賊,而非糾結這塊地是不是我的。
死鎖,定義一般是下在程序上的:死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。在網上找到的解釋大多不分程序執行緒,甚至有寫著寫著就改口的情況。我們知道,程序是計算機分配資源的基本單位,而執行緒是系統獨立排程和分派的基本單位,執行緒不佔有獨立的資源,但上鎖的共享資料之於執行緒一樣能帶來執行緒的死鎖。
2.執行緒安全
執行緒安全的判定圍繞著三個性:原子性,可見性,順序性。
原子性指一件事要麼幹完,要麼全不幹;可見性要求一個執行緒對資料的修改是透明的,即要求大家都能知道;順序性,由《深入理解java虛擬機器》中對
在此理清執行緒安全幾個常見關鍵字: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,為多執行緒程式設計打下牢固基礎。