從零開始學多執行緒之共享物件(二)
想要使用多執行緒程式設計,有一個很重要的前提,那就是必須保證操縱的是執行緒安全的類.
那麼如何構建執行緒安全的類呢? 1. 使用同步來避免多個執行緒在同一時間訪問同一資料. 2. 正確的共享和安全的釋出物件,使多個執行緒能夠安全的訪問它們.
那麼如何正確的共享和安全的釋出物件呢? 這正是這篇部落格要告訴你的.
1. 多執行緒之間的可見性問題.
為什麼在多執行緒條件下需要正確的共享和安全的釋出物件呢?
這要說到可見性的問題:
在多執行緒環境下,不能保證一個執行緒修改完共享物件的資料,對另一個執行緒是可見的.
一個執行緒讀到的資料也許是一個過期資料,這會導致嚴重且混亂的問題,比如意外的異常,髒的資料結構,錯誤的計算和無限的迴圈.
舉個例子:
public class NoVisibility {
private static int num;
private static boolean ready;
private static class RenderThread extends Thread{
@Override
public void run(){
while(!ready){
Thread.yield();
}
System.out.println( "num = " + num);
}
}
public static void main(String [] args) throws InterruptedException {
new RenderThread().start();
num = 42;
ready = true;
}
}
new RenderThread().start()表示建立一個新執行緒,並執行執行緒內的run()方法 ,如果ready的值是false,執行Thread.yield()方法(當前執行緒休息一會讓其他執行緒執行),這時候再交給main方法的主執行緒執行,給num賦值42,ready賦值true,然後在任務執行緒中輸出num的值.因為可見性的問題,任務執行緒可能沒有看到主執行緒對num賦值,而輸出0.
我們接下來來看看釋出物件也會引發的可見性問題.
2. 什麼是釋出一個物件
釋出: 讓物件內被當前範圍之外的程式碼所使用.
public class Publish {
public int num1;
private int num2;
public int getNum2(){
return this.num2;
}
}
無論是 publish.num1 還是 publish.getNum2()哪種方法,只要能在類以外的地方獲取到物件,我們就稱物件被髮布了.
如果一個物件在沒有完成構造的情況下就釋出了,這種情況叫逸出.逸出會導致其他執行緒看到過期值,危害執行緒安全.
常見的逸出的情況:
1.最常見的逸出就是將物件的引用放到公共靜態域(public static Object obj),釋出物件的引用,而在區域性方法中例項化這個物件.
public class Test {
public static Set<Object> set;
public void initialize(){
set = new HashSet<>();
}
}
2.釋出物件的狀態,而且狀態是可變的(沒用final修飾),或狀態裡包含其他的可變資料.
public class UnsafeStates {
private String [] states = new String[]{"a","b","c"};
public String[] getStates(){
return states;
}
}
3.在構造方法中使用內部類. 內部類的例項包含了對封裝實隱含的引用.
public class UnsafeStates {
private Runnable r;
public UnsafeStates() {
r = new Runnable() {
@Override
public void run() { // 內部類在物件沒有構造好的情況下,已經可以this引用,逸出了
// do something;
}
};
}
}
逸出主要會導致兩個方面的問題:
1. 釋出執行緒以外的任何執行緒都能看到物件的域的過期值,因而看到的是一個null引用或者舊值,即使此刻物件已經被賦予了新值.
2. 執行緒看到物件的引用是最新的,但是物件的狀態卻是過期的.我們已經瞭解了逸出的問題,那麼如何安全的釋出一個物件呢?
為了安全地釋出物件,物件的引用以及物件的狀態必須同時對其他執行緒可見(也就是說安全釋出就是保證物件的可見性),一個正確建立的物件可以通過下列條件安全釋出:
1. 通過靜態初始化器初始化物件的引用.
public class NoVisibility {
public static Object obj = new Object();
}
2. 將它的引用儲存到volatile域或AtomicReference;
public class NoVisibility {
public volatile Object obj = new Object();
}
Volatile可以保證可見性.效能消耗也只比非volatile多一點,但是不要過度依賴volatile變數,它比使用鎖的程式碼更脆弱,更難以理解,使用volatile的最佳方式就是用它來做退出迴圈的條件.使用volatile的例子:
public class Cycle {
private boolean condition;
public void loop(){
while (condition){
//do something..
}
}
public void changeCondition(){
if(condition == true){
condition = false;
}else{
condition = true;
}
}
}
3. 將它的引用儲存到正確建立的物件的final域中.
public class NoVisibility {
public final Object obj = new Object();
}
4. 或者將它的引用儲存到由鎖正確儲存的域中.
public class NoVisibility {
private Hashtable<String,Object> hashtable = new Hashtable<>();
public void setHashtable(){
Object obj = new Object();
hashtable.put("obj",obj);
}
}
不限於HashTable,只要是執行緒安全的容器都行
現在我們瞭解瞭如何安全的釋出一個物件,那麼
問題來了,是否所有物件都需要安全釋出?安全釋出的物件是否就是執行緒全的了?
讓我們繼續往下看.
3.如何構建一個執行緒安全的類.
我們先來回答上面的第一個疑問,是否所有物件都需要安全釋出?
答案都是否定的.
要回答這個問題,我們先簡單瞭解一下以下的三種物件:
1.不可變物件
2.高效不可變物件
3.可變物件1.不可變物件:建立後不能被修改的物件叫不可變物件,不可變物件天生是執行緒安全的.
不可變物件不僅僅是所有域都是final型別的,
只有滿足如下狀態才是不可變物件:
1.1
它的狀態不能在建立後改變.(包括狀態包含的其他值也不可做修改,比如狀態是一個集合list,list裡面的值也不可以修改,或者狀態是一個物件,那麼物件的狀態也不更改)
1.2.所有域都是final型別的.1.3.它被正確建立(建立期間沒有this引用的逸出)
用高效不可變物件可以簡化開發,並由於減少了同步的使用,還會提高效能.3. 可變物件: 就是可變物件.
下面就是三種物件的釋出機制,釋出物件的必要條件依賴於物件的可變性:
1. 不可變物件可以通過任意機制釋出;
2. 高效不可變物件必須要安全地釋出;
3. 可變物件必須要安全釋出,同時必須要執行緒安全或者是被鎖保護.
最後一個問題安全釋出的物件是否就是執行緒全的了?
安全釋出只能保證物件釋出時的可見性,所以要保證執行緒的安全就要根據物件的可變性,通過同步+安全釋出來保證執行緒安全.關於同步和執行緒安全的知識可以看我的上一篇部落格
這兩篇部落格的知識點加在一起就可以構建執行緒安全類了.在下一篇部落格中,我會為大家介紹一些構建執行緒安全類的模式,這些模式讓類更容易成為執行緒安全的,並且不會讓程式意外破壞這些類的執行緒安全性.本期分享就到這了,我們下篇再見!