日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

并发编程—Volatile关键字

發布時間:2024/1/17 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并发编程—Volatile关键字 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個線程持有某個特定的鎖,因此可以保證一次就只有一個線程在訪問共享數據。可見性要復雜一些,它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的。

volatile 變量可以被看作是一種 “輕量級的 synchronized”,與 synchronized 塊相比,volatile 變量所需的編碼較少,并且運行時開銷也較少,但是它所能實現的功能也僅是 synchronized 的一部分。

volatile變量

一個共享變量被volatile修飾之后,則具有了兩層語義:

  • 保證了該變量在多個線程的可見性。
  • 禁止了指令重排序
  • 保證內存可見性

    前面講過Java內存模型,可以知道:對一個共享變量進行操作時,各個線程會將共享變量從主內存中拷貝到工作內存,然后CPU會基于工作內存中的數據進行處理。線程在工作內存進行操作完成之后何時會將結果寫回主內存中?這個時機對普通變量是沒有規定的。所以才導致了內存可見性問題。

    volatile是如何解決可見性問題的?
    如果代碼中的共享變量被volatile修飾,在生成匯編代碼時會在volatile修飾的共享變量進行寫操作的時候會多出Lock前綴的指令。在多核處理器的情況下,這個Lock指令主要有3個功能:

  • volatile的變量被修改后會立即寫入到主存中
  • 這個寫回主存的操作會告知其它線程中該變量對應的緩存行失效,所以其它線程如果要操作這個變量,會重新去主存中讀取最新的值。
  • 禁止特定類型的重排序。
  • 所以,被volatile修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象

    禁止指令重排序

    對于volatile的共享變量,編譯器在生成字節碼時,會在指令序列中插入內存屏障(Lock指令)來禁止特定類型的重排序。這是在happens-before的原則下做進一步的約束

    對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎是不可能的,為此,JMM采取了保守策略:

    • 在每個volatile寫操作的前面插入一個StoreStore屏障;
    • 在每個volatile寫操作的后面插入一個StoreLoad屏障;
    • 在每個volatile讀操作的后面插入一個LoadLoad屏障;
    • 在每個volatile讀操作的后面插入一個LoadStore屏障。

    需要注意的是:volatile寫是在前面和后面分別插入內存屏障,而volatile讀操作是在后面插入兩個內存屏障。

    • StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
    • StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
    • LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
    • LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序

    如下兩張圖來自《Java并發編程的藝術》一書:

    • volatile變量的寫操作
    • volatile變量的讀操作

    根據上面的說明也能得出:雖然volatile關鍵字能禁止指令重排序,但是volatile也只能在一定程度上保證有序性。在volatile之前和之后的指令集不會亂序越過volatile變量執行,但volatile之前和之后的指令集在沒有關聯性的前提下,仍然會執行指令重排。

    使用 volatile 變量的條件

    volatile并不能代替synchronized,要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴于當前值
    例如i++的操作就無法通過volatile保證結果準確性的,因為i++包含了讀取-修改-寫入三個步驟,并不是一個原子操作,所以 volatile 變量不能用作線程的安全計數器。
    例如下面的這段代碼,可以說明volatile變量的操作不具有原子性

    package com.lzumetal.multithread.volatiletest;public class Counter {private volatile static int count = 0;private static void inc() {//延遲1毫秒,使得結果明顯sleep(1);count++;}public static void main(String[] args) {//同時啟動1000個線程,去進行i++計算,看看實際結果for (int i = 0; i < 1000; i++) {new Thread(Counter::inc).start();}sleep(1000); System.out.println("運行結果:Counter.count=" + Counter.count); //結果很可能<1000}private static void sleep(long millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {e.printStackTrace();}}}

    運行計數器的結果很大可能性是<1000的。對于計數器的這種功能,一般是需要使用JUC中atomic包下的類,利用CAS的機制去做。

  • 該變量沒有包含在具有其他變量的不變式中
    這句話有點拗口,看代碼比較直觀。

    public class NumberRange {private volatile int lower = 0;private volatile int upper = 10;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;}}
  • 上述代碼中,上下界初始化分別為0和10,假設線程A和B在某一時刻同時執行了setLower(8)和setUpper(5),且都通過了不變式的檢查,設置了一個無效范圍(8, 5),所以在這種場景下,需要使setLower()和setUpper()操作原子化 —— 而將字段定義為 volatile 類型是無法實現這一目的的。

    使用 volatile 舉例

    雖然使用 volatile 變量要比使用相應的鎖簡單得多,而且性能也更好,但是一般不會太多的使用它,主要是它比使用鎖更加容易出錯。
    想要安全地使用volatile,必須牢記一條原則:只有在狀態真正獨立于程序內其他內容時才能使用 volatile

    修飾狀態標志量

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

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

    double-check 單例模式

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

    為什么要用volatile修飾才是最安全的呢?可能有人會覺得是這樣:線程1執行完第4步,釋放鎖。線程2獲得鎖后執行到第4步,由于可見性的原因,發現instance還是null,從而初始化了兩次。
    但是不會存在這種情況,因為synchronized能保證線程1在釋放鎖之前會講對變量的修改刷新到主存當中,線程2拿到的值是最新的。

    實際存在的問題是無序性。
    第4步這個new操作是無序的,它可能會被編譯成:
    a.先分配內存,讓instance指向這塊內存
    b.在內存中創建對象

    synchronized雖然是互斥的,但不代表一次就把整個過程執行完,它在中間是可能釋放時間片的,時間片不是鎖。
    也就是說可能在a執行完后,時間片被釋放,線程2執行到1,此時它讀到的instance是不是null呢?基于可見性,可能是null,也可能不是null。 有意思的是,在這個例子中,如果讀到的是null,反而沒問題了,接下來會等待鎖,然后再次判斷時不為null,最后返回單例。
    如果讀到的不是null,按代碼邏輯直接return instance,但這個instance還沒執行構造參數,所以使用的時候就會出現問題。

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的并发编程—Volatile关键字的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。