volatile的適用場景

介紹

把代碼塊聲明為 synchronized,有兩個重要后果,通常是指該代碼具有 原子性(atomicity)可見性(visibility)

  • 原子性意味著個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互沖突。 所謂原子性操作是指不會被線程調度機子打斷的操作,這種操作一旦開始,就一直到幸運星結束,中間不會有任何切換(切換線程)。
  • 可見性則更為微妙,它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的。 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。

volatile的使用條件:

volatile變量具有 synchronized 的可見性特性,但是不具備原子性。這就是說線程能夠自動發現 volatile 變量的最新值

volatile變量可用于提供線程安全,但是只能應用于非常有限的一組用例:多個變量之間或者某個變量的當前值與修改后值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變量相關的不變式(Invariants)的類(例如 “start <=end”)。

出于簡易性或可伸縮性的考慮,您可能傾向于使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易于編碼和閱讀。此外,volatile 變量不會像鎖那樣造成線程阻塞,因此也很少造成可伸縮性問題。在某些情況下,如果讀操作遠遠大于寫操作,volatile 變量還可以提供優于鎖的性能優勢。

使用條件

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴于當前值。
  • 該變量沒有包含在具有其他變量的不變式中。

實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態,包括變量的當前狀態。

第一個條件的限制使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++)看上去類似一個單獨操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操作需要使x 的值在操作期間保持不變,而 volatile 變量無法實現這點。(然而,如果只從單個線程寫入,那么可以忽略第一個條件。)

反例

大多數編程情形都會與這兩個條件的其中之一沖突,使得 volatile 變量不能像 synchronized 那樣普遍適用于實現線程安全。

【反例:volatile變量不能用于約束條件中】 下面是一個非線程安全的數值范圍類。它包含了一個不變式 —— 下界總是小于或等于上界。

public class NumberRange {  private volatile int lower;private volatile int upper;  public int getLower() { return lower; }  public int getUpper() { return upper; }  public void setLower(int value) {   if (value > upper)   throw new IllegalArgumentException(...);  lower = value;  }  public void setUpper(int value) {   if (value < lower)   throw new IllegalArgumentException(...);  upper = value;  }  
}

lower 和 upper 字段定義為 volatile 類型不能夠充分實現類的線程安全;而仍然需要使用同步——使 setLower()setUpper() 操作原子化。

否則,如果湊巧兩個線程在同一時間使用不一致的值執行 setLowersetUpper 的話,則會使范圍處于不一致的狀態。例如,如果初始狀態是(0, 5),同一時間內,線程 A 調用setLower(4) 并且線程 B 調用setUpper(3),顯然這兩個操作交叉存入的值是不符合條件的,那么兩個線程都會通過用于保護不變式的檢查,使得最后的范圍值是(4, 3) —— 一個無效值。

volatile的適用場景

模式 #1:狀態標志

也許實現 volatile 變量的規范使用僅僅是使用一個布爾狀態標志,用于指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;  ...  public void shutdown() {   shutdownRequested = true;   
}  public void doWork() {   while (!shutdownRequested) {   // do stuff  
    }  
}

線程1執行doWork()的過程中,可能有另外的線程2調用了shutdown,所以boolean變量必須是volatile。

而如果使用 synchronized 塊編寫循環要比使用 volatile 狀態標志編寫麻煩很多。由于 volatile 簡化了編碼,并且狀態標志并不依賴于程序內任何其他狀態,因此此處非常適合使用 volatile。

這種類型的狀態標記的一個公共特性是:通常只有一種狀態轉換shutdownRequested 標志從false 轉換為true,然后程序停止。這種模式可以擴展到來回轉換的狀態標志,但是只有在轉換周期不被察覺的情況下才能擴展(從falsetrue,再轉換到false)。此外,還需要某些原子狀態轉換機制,例如原子變量。

模式 #2:一次性安全發布(one-time safe publication)

在缺乏同步的情況下,可能會遇到某個對象引用的更新值(由另一個線程寫入)和該對象狀態的舊值同時存在。

這就是造成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的情況下進行讀操作,產生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的對象。如下面介紹的單例模式。

private  static Singleton instace;     public static Singleton getInstance(){     //第一次null檢查       if(instance == null){              synchronized(Singleton.class) {    //1       //第二次null檢查         if(instance == null){          //2    instance = new Singleton();//3    
            }    }             }    return instance;   
}

模式 #3:獨立觀察(independent observation)

安全使用 volatile 的另一種簡單模式是:定期 “發布” 觀察結果供程序內部使用。【例如】假設有一種環境傳感器能夠感覺環境溫度。一個后臺線程可能會每隔幾秒讀取一次該傳感器,并更新包含當前文檔的 volatile 變量。然后,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值。

使用該模式的另一種應用程序就是收集程序的統計信息。

【例】如下代碼展示了身份驗證機制如何記憶最近一次登錄的用戶的名字。將反復使用lastUser 引用來發布值,以供程序的其他部分使用。(主要利用了volatile的可見性)

public class UserManager {  public volatile String lastUser; //發布的信息  public boolean authenticate(String user, String password) {  boolean valid = passwordIsValid(user, password);  if (valid) {  User u = new User();  activeUsers.add(u);  lastUser = user;  }  return valid;  }  
}
模式 #4:“volatile bean” 模式

volatile bean 模式的基本原理是:很多框架為易變數據的持有者(例如 HttpSession)提供了容器,但是放入這些容器中的對象必須是線程安全的。

在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的,并且 getter 和 setter 方法必須非常普通——即不包含約束!

public class Person {  private volatile String firstName;  private volatile String lastName;  private volatile int age;  public String getFirstName() { return firstName; }  public String getLastName() { return lastName; }  public int getAge() { return age; }  public void setFirstName(String firstName) {   this.firstName = firstName;  }  public void setLastName(String lastName) {   this.lastName = lastName;  }  public void setAge(int age) {   this.age = age;  }  
}
模式 #5:開銷較低的“讀-寫鎖”策略

如果讀操作遠遠超過寫操作,您可以結合使用內部鎖volatile 變量來減少公共代碼路徑的開銷。

如下顯示的線程安全的計數器,使用 synchronized 確保增量操作是原子的,并使用 volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優于一個無競爭的鎖獲取的開銷。

public class CheesyCounter {  // Employs the cheap read-write lock trick  // All mutative operations MUST be done with the 'this' lock held  @GuardedBy("this") private volatile int value;  //讀操作,沒有synchronized,提高性能  public int getValue() {   return value;   }   //寫操作,必須synchronized。因為x++不是原子操作  public synchronized int increment() {  return value++;  }  
}

使用鎖進行所有變化的操作,使用 volatile 進行只讀操作。
其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執行讀操作。

單例模式

定義:

確保某個類只有一個實例,并提供一個全局訪問點。

類圖:

1364026153_1078.gif

public class Singleton{  private static final Singleton instance;  private Singleton(){  }  public static Singleton getInstance(){      if(instance == null){          //1  instance = new Singleton();//2  
        }  return instance;               //3  
   }  ...  
}

優點:

  1. 內存中只有一個對象,減少內存開支;
  2. 單例可避免對資源的多重占用,例如寫文件動作,可避免對同一資源文件的同時寫操作。

缺點:

  1. 單例模式一般沒有接口,擴展很困難; ——單例并不是用來繼承的。
  2. 不利于測試,并行開發時,若單例未完成,則不能進行測試;
  3. 與單一職責原則沖突,把“要單例”和業務邏輯融合在一個類中。

使用場景:

若出現多個對象就會出現“不良反應”,應該用單例,具體場景如下:

  1. 要求生成唯一序列號的環境;
  2. 在整個項目中需要一個共享訪問點或共享數據。例如頁面計數器;
  3. 創建一個對象需要消耗的資源過多時;
  4. 需要定義大量的靜態常量和靜態方法的環境。

為什么不直接用全局變量來實現單例?

有缺點:全局變量必須在程序一開始就創建好。而單例模式可以延遲初始化。

類加載器對單例的影響:

不同的類加載器可能會加載同一個類。

如果程序有多個類加載器,可在單例中指定某個加載器,并指定同一個加載器。

多線程的影響:

上文代碼示例在多線程環境下有bug:

  1. 線程 1 調用 getInstance() 方法并決定 instance 在 //1 處為null
  2. 線程 1 進入 if 代碼塊,但在執行 //2 處的代碼行時被線程 2 預占。
  3. 線程 2 調用 getInstance() 方法并在 //1 處決定 instancenull
  4. 線程 2 進入 if 代碼塊并創建一個新的 Singleton 對象并在 //2 處將變量instance 分配給這個新對象。
  5. 線程 2 在 //3 處返回 Singleton 對象引用。
  6. 線程 2 被線程 1 預占。
  7. 線程 1 在它停止的地方啟動,并執行 //2 代碼行,這導致創建另一個 Singleton 對象。
  8. 線程 1 在 //3 處返回這個對象。
結果是 getInstance() 方法創建了兩個 Singleton 對象。

解決方法一:不用延遲初始化

public class Singleton{  private static final Singleton instance = new Singleton();  private Singleton(){  }  public static Singleton getInstance(){           return instance;  }  ...  
}

解決方法二:同步getInstance

public class Singleton{  private static final Singleton instance;  private Singleton(){  }  //同步getInstance  public static synchronized Singleton getInstance(){      if(instance == null){          //1  instance = new Singleton();//2  
        }  return instance;               //3  
   }  ...  
}

但是synchronized方法會降低性能,尤其這里僅當第一次調用getInstance時才需要同步,只有執行//2代碼行時才需要同步。

你可能想到只同步方法塊,即只對//2進行同步:

public static Singleton getInstance(){      if(instance == null){            synchronized(Singleton.class) {    instance = new Singleton();  }  }   return instance;  
}

但這樣做并不能解決問題:
當 instance 為 null 時,兩個線程可以并發地進入if 語句內部。
然后,一個線程進入 synchronized 塊來初始化 instance,而另一個線程則被阻斷。

??? 當第一個線程退出 synchronized 塊時,等待著的線程進入并創建另一個Singleton 對象。

注意:當第二個線程進入 synchronized 塊時,它并沒有檢查 instance 是否非 null。
還是會創建2個對象。

解決方法三:雙重檢查加鎖

針對上述方法的缺點,我們在//2代碼行時 再檢查一次null,就能保證只創建一個對象:

 
private  static Singleton instace;   public static Singleton getInstance(){   //第一次null檢查     if(instance == null){            synchronized(Singleton.class) {    //1     //第二次null檢查       if(instance == null){            //2  instance = new Singleton();//3  
            }  }           }  return instance;
}

假設有下列事件序列:

  1. 線程 1 進入 getInstance() 方法。
  2. 由于 instance 為 null,線程 1 在 //1 處進入synchronized 塊。
  3. 線程 1 被線程 2 預占。
  4. 線程 2 進入 getInstance() 方法。
  5. 由于 instance 仍舊為 null,線程 2 試圖獲取 //1 處的鎖。然而,由于線程 1 持有該鎖,線程 2 在 //1 處阻塞。
  6. 線程 2 被線程 1 預占。
  7. 線程 1 執行,由于在 //2 處實例仍舊為 null,線程 1 還創建一個Singleton 對象并將其引用賦值給instance(由于java執行的無序性,可能賦值時只是占用內存空間(此時instance已經為非null,鎖松開,由于無序性,還沒有來得及初始化,線程2已經取得instance對象),還沒有根據構造函數初始化)。
  8. 線程 1 退出 synchronized 塊并從 getInstance() 方法返回實例。
  9. 線程 1 被線程 2 預占。
  10. 線程 2 獲取 //1 處的鎖并檢查 instance 是否為 null。
  11. 由于 instance 是非 null 的,并沒有創建第二個Singleton 對象,由線程 1 創建的對象被返回,此時返回對象可能是是一個構造完整卻沒有完全初始化的對象。
  12. 線程1繼續執行完成對象的初始化,由于instance是volatile類型的,所以instance變量對所有線程共享可見,所以線程2可以得到一個完整初始化的對象。

對于上面解說的賦值,卻沒有初始化的原因,是由于java變量重新賦值時有3個步驟的(讀取,修改,回寫)

代碼行 instance =new Singleton(); 執行了下列偽代碼

1. mem = allocate();             //Allocate memory for Singleton object.
2. instance = mem;               //Note that instance is now non-null, but//has not been initialized.
3. ctorSingleton(instance);      //Invoke constructor for Singleton passing//instance.

轉載于:https://www.cnblogs.com/ouyxy/p/7242563.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/252222.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/252222.shtml
英文地址,請注明出處:http://en.pswp.cn/news/252222.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

link和@import的區別

1、link屬于HTML標簽&#xff0c;import是css提供的 2、link是HTML標簽&#xff0c;沒有兼容問題&#xff0c;而import只在IE5以上才能識別 3、頁面被加載時&#xff0c;link會同時被加載&#xff0c;而import引用的css會等到頁面加載完再加載 4、link方式的樣式的權重高于impo…

6.java 代碼塊

代碼塊 在java中用{}括起來的稱為代碼塊&#xff0c;代碼塊可分為以下四種: 普通代碼塊構造代碼塊靜態代碼塊同步代碼塊普通代碼塊 在方法或語句中出現的{}就稱為普通代碼塊。普通代碼塊和一般語句的執行順序由他們在代碼中出現的次序決定&#xff0c;先出現先執行。 普通代碼塊…

C#如何測試代碼運行時間

第一種方式&#xff1a;System.Diagnostics.Stopwatch stopwatch new Stopwatch(); stopwatch.Start(); // 開始監視代碼運行時間 // 需要測試的代碼 .... stopwatch.Stop(); // 停止監視 TimeSpan timespan stopwatch.Elapsed; // 獲取當前實例測量得出的總時間 double …

0074 幾道面試題

昨天參加了惠裝網的面試&#xff0c;有些題不會做的&#xff0c;記錄下來 switch語句能否作用在byte、long、String上 Java1.7以前&#xff1a;byte、short、int、char Java1.7開始&#xff1a;新增String 因此switch語句不能作用在long上&#xff0c;看下面代碼&#xff1a; p…

SpringBoot入門之內嵌Tomcat配置

spring boot默認web程序啟用tomcat內嵌容器tomcat&#xff0c;監聽8080端口,servletPath默認為 / 。需要用到的就是端口、上下文路徑的修改&#xff0c;在spring boot中其修改方法極其簡單&#xff0c;實例如下&#xff1a; server.port8088 server.context-path/test 啟動程序…

第二十二章:動畫(六)

復合動畫您可以混合等待和未等待的調用來創建復合動畫。 例如&#xff0c;假設您希望按鈕在大小擴展的同時旋轉360度然后收縮。ViewExtensions類定義一個方法名稱ScaleTo&#xff0c;它為Scale屬性設置動畫&#xff0c;就像RotateTo為Rotate屬性設置動畫一樣。 Button大小的擴展…

C#操作Excel總結

0. 導入命名空間&#xff1a; 1234using Microsoft.Office.Core;using Microsoft.Office.Interop.Excel;using System.IO;using System.Reflection;1. 如何打開已有excel文檔&#xff0c;或者創建一個新的excel文檔 123Application app new Application();Workbooks wbks app…

Ubuntu16.04用源安裝Nginx+PHP5.6+MySQL5.6

安裝Nginx 1、首先添加nginx_signing.key(必須&#xff0c;否則出錯) $ wget http://nginx.org/keys/nginx_signing.key$ sudo apt-key add nginx_signing.key 2、添加]Nginx](http://nginx.org/)官方提供的源 $ echo "deb http://nginx.org/packages/ubuntu/ trusty ngin…

leetcode -39組合總數

搜就完事了&#xff0c;沒想著優化。唉~太菜&#xff0c;給一個位置標記位置&#xff0c;然后通過該位置向該位置及該位置以下尋找&#xff0c;這樣不存在什么重復回去查找問題。 如果總結大于目標值&#xff0c;回溯一下&#xff0c;如果不大于繼續。 class Solution { public…

避免某個子窗體重復運行的方法(showdialog、show)

在C#中窗口的顯示有兩種方式&#xff1a;模態顯示&#xff08;showdialog&#xff09;和非模態顯示&#xff08;show&#xff09;。 二者最常見的區別是&#xff1a;模態顯示后&#xff0c;彈出窗口阻止調用窗口的所有消息響應。只有在彈出窗口結束后調用窗口才能繼續。在模態窗…

ubantu之Git使用

本文講述在Ubuntu 14.04 x64環境下&#xff0c;如何安裝Git&#xff0c;配置連接GitHub&#xff0c;并且上傳本地代碼到github。 一. 注冊Git賬戶以及創建倉庫 要想使用github第一步當然是注冊github賬號了。之后就可以創建倉庫了&#xff08;免費用戶只能建公共倉庫&#xff0…

Java中基礎數據類型分類

Java中的四類八種基本數據類型 第一類&#xff1a;整數類型 byte short int long &#xff08;int是整形&#xff0c;也屬于整數類型&#xff09; 第二類&#xff1a;浮點型 float double 第三類&#xff1a;邏輯型 boolean(它只有兩個值可取true false) 第四類&#xff1…

C#如何打包EXE程序生成setup安裝文件

C#如何打包EXE程序生成setup安裝文件作為研發人員&#xff0c;在本機上開發的winform wpf或者控制臺程序需要發給其他人測試時候&#xff0c;一般需要對其進行打包生成setup安裝文件&#xff0c;今天第一次&#xff0c;搜了下資料&#xff0c;記錄如下&#xff1a;注&#xff1…

PHP正則表達式

php正則表達示的定界符 PHP的正則表達示定界符的規定如下&#xff1a; 定界符&#xff0c;不能用a-z A-Z 0-9 其他的都可以用。必須成對出現&#xff0c;有開始就有結束。 我們來例幾個例子&#xff1a; /中間寫正則/ 正確%中間寫正則% 正確^中間寫正則^ 正確中間寫正則 正確(…

最具戲劇性的分析診斷案例——十分鐘鎖定數據庫性能“元兇”

昨天&#xff0c;正好有點空時間想看看書&#xff0c;結果&#xff0c;剛打開書&#xff0c;沒看幾個字兒&#xff0c;接到用戶電話說&#xff1a;一個庫有問題&#xff0c;希望能幫忙看下。因為我知道他們那邊也有自己的專職DBA&#xff0c;于是問&#xff1a;沒讓人給看看嗎&…

Python黑科技:在家遠程遙控公司電腦,python+微信一鍵連接!

有時候需要遠程家里的臺式機使用&#xff0c;因為我平時都是用 MAC 多&#xff0c;但是遠程喚醒只能針對局域網&#xff0c;比較麻煩&#xff0c;于是我想用微信實現遠程喚醒機器。 *注意&#xff1a;全文代碼可左右滑動查看 準備工作 本程序主要是實現遠程管理 Windows10操作系…

c#通過app.manifest使程序以管理員身份運行

通常我們使用c#編寫的程序不會彈出這個提示&#xff0c;也就無法以管理員身分運行。微軟的操作系統使用微軟的產品方法當然是有的&#xff0c;通過app.manifest配置可以使程序打開的時候&#xff0c;彈出UAC提示需要得到允許才可以繼續&#xff0c;這樣就獲得了管理員的權限來執…

Oracle 作業

Oracle 作業 dbms_job與 dbms_scheduler 用于安排和管理作業隊列,通過使用作業,可以使ORACLE數據庫定期執行特定的任務。 一. dbms_job 1.1. 創建 variable jobno number; begin dbms_job.submit(:jobno,proce_t;, sysdate, sysdate1/24/60); commit; end; / 1.2. 參數 Job 輸出…

企業級 Spring Boot 教程 (十四)用restTemplate消費服務

構架工程 創建一個springboot工程&#xff0c;去消費RESTFUL的服務。這個服務是 http: ///gturnquist-quoters.cfapps.io/api/random &#xff0c;它會隨機返回Json字符串。 Spring Cloud大型企業分布式微服務云架構源碼請加一七九一七四三三八零 在Spring項目中&#xff0c;它…

MOS管基本認識(快速入門)

1. 三個極的判定G極(gate)—柵極&#xff0c;不用說比較好認 S極(source)—源極&#xff0c;不論是P溝道還是N溝道&#xff0c;兩根線相交的就是 D極(drain)—漏極&#xff0c;不論是P溝道還是N溝道&#xff0c;是單獨引線的那邊2. N溝道與P溝道判別箭頭指向G極的是N溝道 箭頭背…