併發編程中你們最熟悉的應該是volatile和synchronized了,使用最多的場景應該是單例吧。看代碼,你以爲下邊這個單例有問題嗎? 圖1:java
public class UserManager{
private static UserManager sUserManager;
private UserManager(){}
public static UserManager getInstance(){
if(sUserManager == null){//1
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();
}
}
}
return sUserManager;
}
}
複製代碼
乍一看好像沒什麼毛病,有點經驗的人仔細再一看,誒發現少了一個volatile。標準的寫法好像應該長下邊這樣:
圖2:編程
public class UserManager{
private static volatile UserManager sUserManager;
private UserManager(){}
public static UserManager getInstance(){
if(sUserManager == null){//2
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();// 3
}
}
}
return sUserManager;
}
}
複製代碼
那問題就來了,加這個volatile有什麼用?不是已經加了synchronized了嗎?不加不行嗎? 看過不少文章,模糊記得volatile是用來保證可見性的。併發
一個線程寫共享變量,在另一個線程中能當即可見。好比圖2(在volatile關鍵字下)線程A執行完3位置的代碼後,這個時候線程B開始進行2位置的條件判斷,就能立馬看見sUserManager的值不爲null。若是如圖1(沒有volatile加持),並不能保證這種可見性。那你可能會想synchronized難道不保證可見性嗎?答案是:正確使用synchronized同步是能夠保證可見性的。
以下圖3:優化
public class UserManager{
private static UserManager sUserManager;
private UserManager(){}
public synchronized static UserManager getInstance(){//4
if(sUserManager == null){
sUserManager = new UserManager();
}
return sUserManager;
}
}
複製代碼
全部共享變量的訪問必須由synchronized關鍵字同步。 同一個對象上,synchronized代碼塊能保證訪問共享變量,是其餘線程離開synchronized同步代碼的操做結果,也就保證了可見性。 圖1中1位置代碼並無synchronized同步,那麼訪問這個地方的代碼就多是失效的。圖3中對sUserManager變量進行了正確的同步。spa
其實可見性並非圖1單例寫法的癥結。圖1問題重點在於訪問到位置1代碼的時候,sUserManager不爲null,可是UserManager尚未初始化完成。出現這種狀況在於編譯優化的時候代碼可能會重排序。
如圖4線程
public class UserManager{
private static UserManager sUserManager;
private String name;//6
private int age;//7
private UserManager(){
name = "聰聰";
age = 1;
}
public static UserManager getInstance(){
if(sUserManager == null){//8
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();// 5
}
}
}
return sUserManager;
}
}
複製代碼
線程A執行位置5代碼後,按照直覺,應該位置6位置7代碼已經執行完畢。事實上,通過編譯優化後,線程A執行過位置5代碼後,線程B在代碼8位置處可能看到了sUserManager不爲null,可是name和age還沒初始化,這個時候sUserManager是個無效的值。1> 因爲volatile可以禁止對共享變量的代碼重排,因此圖2中的單例是正確的。2> 正確的同步(全部共享變量的訪問必須由synchronized關鍵字同步。)並不能禁止代碼重排,可是能防止訪問到失效的值,因此圖3是正確的。code