8.1 線程安全
“當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象就是線程安全的。”
這裏作者把Java的線程安全分爲五個等級:
1.不可變:
不可變的對象一定是線程安全的,前提是在構建過程未逃逸。final修飾的對象就是不可變對象。final修飾基本類型就不可變,修飾對象的話對象不可重新初始化,只能調用其中方法,個人理解是內存起始不可變。
2.絕對線程安全:
難。
但作者給的示例代碼已經失效了,科技在進步。只會不停地跑下去,還是安全的。
while(true){
for(int i = 0; i < 10; i++){
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < vector.size(); i++) vector.remove(i);
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < vector.size(); i++) System.out.println(vector.get(i));
}
});
removeThread.start();
printThread.start();
System.out.println(" current have " + Thread.activeCount() );
while(Thread.activeCount() > 20) System.out.println(" already have " + Thread.activeCount() );
}
3.相對線程安全:
通常意義的線程安全,不需要額外加入同步限制。Java的Vector,HashTable都是線程安全的。
4.線程兼容:
對象本身不是線程安全的,在加入同步手段後可以安全地使用。基本都是這樣的。
5.線程對立:
無論加入什麼措施都無法多線程併發。Thread的suspend()和resume()方法就是線程對立的。
8.2 線程安全的實現方法
互斥同步(Mutual Exclusion & Synchronization),阻塞式同步,是一種保證一個對象同一時間只在一個線程中被使用的方法。利用互斥(臨界區、互斥量、信號量)來實現同步。synchronized在java中是一個重量級操作,在有必要的時候才使用。
除了synchronized,還有ReentrantLock可以實現同步。基於同步的基礎上,重入鎖加入三中功能:等待可中斷,持有鎖的線程長時間不釋放鎖,等待的線程可以放棄等待,去做其他事情;公平鎖,多個線程同時等待一個鎖時,按照申請鎖的時間依次獲得鎖,synchronized不行;鎖綁定多個條件,一個ReentrantLock可以綁定多個Condition對象,使用newCondition()就可以添加,不像synchronized每增加一個條件都要添加一個鎖。
非阻塞同步與互斥同步不同,互斥同步在發生鎖時,會立刻處於等待階段,阻塞狀態。非阻塞同步是一種樂觀鎖,先操作,再看該對象是否被多線程競爭,如果被競爭,則採取補救措施(重試),否則就繼續。
無同步方案指一些天生線程安全的方法,不需要同步來保護。可重入代碼(Reentrant Code),也叫純代碼(Pure Code),可以在任何地方被打斷,且重入後不會出現錯誤。線程本地存儲(Thread Local Storage),如果把有競爭的變量都統一放在一個線程中,則無需同步。
8.3 鎖優化
8.3.1 自旋鎖與自適應自旋
由於線程的總調度要交給內核線程,所以線程阻塞的時候的掛起和恢復操作都要轉入內核態來完成,是非常效率低的。且大多數情況等待時間較短,沒必要切換狀態。爲了減少這種狀態切換,可以讓線程執行一個忙自旋,讓線程在等待時“原地踏步”,這個技術是自旋鎖。自旋等待避免了狀態切換,但會佔CPU時間,在等待時間較短時,是非常划算的。因此自旋鎖的自旋時間需要有限制,不能沒完沒了地原地踏步。
自適應自旋指自旋的次數根據上次在同一個鎖上的自旋時間決定,如果上一次成功獲得鎖,且持有鎖的線程正在運行,代表本次自旋是值得的,時間長一些也可以;如果某個鎖自旋很少成功過,則本次可能不選擇自旋。
8.3.2 鎖消除
鎖消除指自動消除一些“沒必要”的鎖。如果一個對象被同步,但檢測到不存在對其數據的競爭,那本次的鎖時無必要的,可以消除。如果一段代碼的所有數據都不會逃逸,則他們是線程私有的,很安全,沒必要上鎖。然而代碼不上鎖不代表運行時無同步,有時Java的內在類會包裝一些同步措施,在這個技術下可以被摘除。
8.3.3 鎖粗化
通常來說,爲了節省阻塞時間,同步範圍越小越好。然而一個對象反反覆覆被同步,且某程序塊都會被反覆競爭,則可以將同步塊的範圍擴大。
8.3.4 輕量級鎖
虛擬機的對象的佈局中對象頭(Object Head)分爲兩個部分,第一個部分用於存儲對象自身的運行時數據,如HashCode,Generation GC Age等,官方稱爲“Mark Word”;另一部分用於存儲指向方法區對象類型數據的指針。爲了節省空間,Mark Word的空間不是固定的,是可伸縮可複用的。
簡單來說,輕量級鎖的原理就是在當前線程中建立一塊Lock Record空間,存儲同步對象的Mark Word,然後讓該對象的Mark Word指向Lock Record中的指針,這樣線程就有了該對象的鎖。如果更新失敗,虛擬機檢查對象的Mark Word是否指向該線程的棧幀,如果有鎖,則該線程擁有了該對象的鎖,否則該對象在被其他線程鎖定了。如果存在兩條線程以上的線程競爭同一個鎖,則輕量級鎖要升級到重量鎖,此時Mark Word中存儲重量級鎖(互斥量)的指針,等待鎖的線程進入等待狀態。
在解鎖中,對象的Mark Word指向棧幀中Lock Record中的指針,將兩指針調換,則解除鎖定。爲啥叫輕量級鎖?因爲很多時候雖然同步了,但不一定有競爭。但真的有了競爭,輕量級鎖使用了CAS來加鎖解鎖,速度倒慢了。所以也不可能取代重量鎖。
8.3.5 偏向鎖
在無競爭的情況下,直接消除整個同步。偏向鎖的意思是鎖會偏向第一個獲取鎖的線程,在他的執行過程中,如果沒有別的線程競爭此對象,則不同步。偏向鎖提高了無競爭,且有同步的程序性能。
至此,本書結束。其實就編程而言,並無太多實用之處,更適合一些硬核的底層設計人員。但開卷有益,也許今日覺得無用的東西,在哪天突然見到了實用性,就會知道回頭看看,也是不錯的。