Volatile


內容摘自 嗶哩嗶哩 尚硅谷視頻: https://space.bilibili.com/302417610/channel/seriesdetail?sid=457613

 java.util.concurrent包下的類

 

談談對Volatile的理解

Volatile不保證原子性代碼驗證

 

 

談談對Volatile的理解

Volatile java虛擬機提供的輕量級的同步機制

  三大特性:保證可見性,不保證原子性、禁止指令重排

談談JMM(Java內存模型 Java Memory Model)

本身是一種抽象的概念,它描述的是一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式

同步的規定:

l 線程解鎖前,必須把共享變量的值刷新回主內存。

l   線程加鎖前,必須讀取主內存的最新值到自己的工作內存

l   加鎖解鎖是同一把鎖

 

由於JVM運行程序的實體是線程,而每個線程創建時JVM都為其創建一個工作內存(有的地方稱為棧空間),工作內存是每個線程的私有數據區域,而java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,

首先要將變量從主內存拷貝到自己的工作空間,然后對變量進行操作,操作完成后再將變量寫回主內存,

不能直接操作主內存的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:

 

 

 

 

三大特性:可見性、原子性、有序性

Volatile可見性代碼驗證

public class MyTest {
    private static boolean flag = true;
//    private static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "start");
                try {
                    TimeUnit.SECONDS.sleep(3);   // 等價於 Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
                System.out.println(Thread.currentThread().getName() + "end");
            }
        },"thread_A").start();

        while (flag) {
//            Thread.sleep(1);
/* 讓while循環里 線程休眠時,不加 volatile,主內存的flag也會刷新到該線程的工作內存,但不是 實時刷新的
 加了 volatile 會實時監聽主內存的flag值的改變,只要改變就會立即將值刷新到該線程的工作內存(該線程不再從工作內存獲取flag值,而是從主內存獲取,然后更新到工作內存)
*/
        }
        System.out.println("結束");
    }
}

 

 

Volatile不保證原子性代碼驗證

public class MyTest {
    private static volatile int num = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) { 
                        num++;  //分三步   拿到num 、進行num+1 、將num+1 賦值給num
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {//保證前面的線程都執行完畢
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }
}

 

如何解決原子性問題

  使用synchronized

   synchronized ("A") {
    num++;  //分三步   拿到num 、進行num+1 、將num+1 賦值給num
}

  使用JUCatomic(原子的)包下的類

 

 

 

 

public class MyTest {
    private static volatile int num = 0;
    private static AtomicInteger num1 = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                            num++;  //分三步   拿到num 、進行num+1 、將num+1 賦值給num
                            num1.getAndIncrement(); //  獲取並加一
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {//保證前面的線程都執行完畢
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);  // 結果:小於 20000 (不是一定小於)
        System.out.println(Thread.currentThread().getName() + num1);// 結果:等於 20000
    }
}

有序性

 計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分一下三種

 

單線程環境里面確保程序最終執行結果和代碼順序執行的結果一致 (不存在指令重排問題)

處理器在進行重排序時必須要考慮指令之間的數據依賴性

多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。

指令重排在多線程中存在的問題

1

 

 

 

 

2

  

 

 

 

變量通過volatile修飾,來禁止指令重排,解決 指令重排在多線程中的問題。

內存屏障

 

線程安全性獲得保證:

 工作內存與主內存同步延遲現象導致的可見性問題

 可以使用synchronizedvolatile關鍵字解決,都可以使一個線程修改后的變量立即對其他線程可見。

 

對於指令重排導致的可見性問題和有序性問題

可以利用volatile關鍵字解決,因為volatile的另一個作用就是禁止重排序優化。

單例模式在多線程環境下存在的問題

DCL(Double Check Lock 雙重校驗鎖)機制不一定安全,原因是有指令重排的存在,加入volatile可以禁止指令重排

 

 

 

 

public class MySingleton {
    private static volatile MySingleton singletonInstance = null;
    private MySingleton() {
        System.out.println(Thread.currentThread().getName() + "構造方法");
    }
    public static MySingleton getMySingletonObj() {
        if (singletonInstance == null) {
            synchronized (MySingleton.class) {
                if (singletonInstance == null) {
                    singletonInstance = new MySingleton();
                }
            }
        }
        return singletonInstance;
    }
}

MESI(緩存一致性協議) & 總線風暴

引用:CSDN博主「敖 丙」的原創文章 :原文鏈接:https://blog.csdn.net/qq_35190492/article/details/105837982

為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSIMESIIllinoisProtocol)、MOSISynapseFireflyDragonProtocol等。

MESI(緩存一致性協議)

CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那么它就會從內存重新讀取。

嗅探

每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。

總線風暴

由於VolatileMESI緩存一致性協議,需要不斷的從主內存嗅探和cas不斷循環,無效交互會導致總線帶寬達到峰值。

所以不要大量使用Volatile,至於什么時候去使用Volatile什么時候使用鎖,根據場景區分。

volatilesynchronized的區別

volatile只能修飾實例變量和類變量,而synchronized可以修飾方法,以及代碼塊。

volatile保證數據的可見性,但是不保證原子性(多線程進行寫操作,不保證線程安全);synchronized是一種排他(互斥)的機制。 volatile用於禁止指令重排序:可以解決單例雙重檢查對象初始化代碼執行亂序問題。

volatile可以看做是輕量版的synchronizedvolatile不保證原子性,但是如果是對一個共享變量進行多個線程的賦值,而沒有其他的操作,那么就可以用volatile來代替synchronized,因為賦值本身是有原子性的,而volatile又保證了可見性,所以就可以保證線程安全了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM