volatile關鍵字與內存可見性

一、volatile關鍵字與內存可見性

1、測試沒有 volatile關鍵字的demo

public class VolatileTest1 {

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while(true){
            if(threadDemo.isFlag()){
                System.out.println("----主線程讀到flag爲true----");
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable {
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }
        flag = true;
        System.out.println("子線程修改了值flag=" + isFlag());
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

     

在子線程中將線程的共享變量 flag的值修改成了 true時,但是主線程在條件判斷時讀到的flag一直是false,所以while循環不會停止跳出,程序不會終止。這是由於內存的可見性導致的。

2、內存可見性(Memory Visibility)

內存可見性(Memory Visibility)其實是指共享變量在不同線程之間的可見性。

  • 共享變量:如果一個變量在多個線程的工作內存中都存在副本,那麼這個變量就是這幾個線程的共享變量,即通常稱這種被多個線程訪問的變量爲共享變量。
  • 可見性:指當某個線程正在使用共享變量並對共享變量的值做了修改時,能夠及時的被其他線程看到共享變量的變化。

 

內存可見性與Java內存模型有關係

所有的變量都存儲在主內存中(操作系統給進程分配的內存空間),而每個線程都有自己獨立的工作內存,裏面保存該線程使用到的變量的副本。

    

注意:線程對共享變量的所有操作都必須在自己的工作內存(working memory,是cache和寄存器的一個抽象,並不是內存中的某個部分)。不同線程之間,當前線程無法直接訪問其他線程的工作內存中的變量,線程間變量值得傳遞需要通過主內存來完成。

緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。
 

 

解決共享變量的內存可見問題的方式有很多

1、synchronized實現可見性

JMM(Java內存模型)關於synchronized的兩條規定:

線程解鎖前,必須把共享變量的最新值刷新到主內存中,

線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while(true){
            // main線程加鎖
            synchronized(threadDemo){
                if(threadDemo.isFlag()){
                    System.out.println("----主線程讀到flag爲true----");
                    break;
                }
            }
        }
    }

2、volatile關鍵字實現可見性

對於多線程, volatile不具備“互斥性”,不能保證變量狀態的“原子性操作”。

使用 volatile 關鍵字用來確保將變量的更新操作通知到其他線程。

某個線程的工作內存中修改了共享變量的值並會刷新到主內存中,同時其他線程已經讀取的共享變量副本就會失效,需要讀數據時就會再次去主內存中讀取新的共享變量的值,從而達到共享變量內存可見。

 // 共享變量用 volatile修飾即可   
 private volatile boolean flag = false;

    

synchronized 和 volatile比較

synchronized具備“互斥性”,既能保證可見性,又能保證原子性,volatile不具備“互斥性”,只能保證可見性,不能保證原子性。

volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程,效率更高。如果能用 volatile解決問題,應儘量使用volatile,因爲它的效率比synchronized更高。

 

二、原子性

原子性:一次操作,要麼全部執行成功,要麼全部執行失敗。一個很經典的例子就是銀行賬戶轉賬問題。

1、一個實例demo

public class AtomicTest {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo(0);
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}

class AtomicDemo implements Runnable{
    //線程共享變量
    private volatile int number;

    public AtomicDemo(int number) {
        this.number = number;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ",number=" + ++number);
    }
}

       

運行結果會發現可能會在不同的線程中,看到相同的數值,這是由於 volatile關鍵字保證了操作的內存可見性,但是 volatile不能保證操作的原子性。

自增操作不是原子性操作,它包括讀取變量的原始值、進行加1操作、寫入工作內存。而且volatile也無法保證對變量的任何操作都是原子性的。

2、解決原子性操作問題--JUC

java.util.concurrent.atomic 原子操作類包裏面提供了一組原子變量類。封裝了一系列常用的數據類型對應的封裝類,

Java.util.concurrent.atomic 中實現的原子操作類可以分成4組:

標量類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

複合變量類:AtomicMarkableReference,AtomicStampedReference

這些類都保證了兩點:

1)類裏的變量都用了volatile保證內存是可見的

2)使用了一個算法CAS(Compare And Swap),保證對這些數據的操作具有原子性

public class AtomicTest1 {

    public static void main(String[] args) throws InterruptedException {
        //線程共享變量
        AtomicInteger atomicInteger = new AtomicInteger(0);
        AtomicDemo1 atomicDemo = new AtomicDemo1(atomicInteger);
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(atomicDemo);
            thread.start();
        }
    }
}

class AtomicDemo1 implements Runnable{
    private AtomicInteger atomicInteger = null;

    public AtomicDemo1(AtomicInteger atomicInteger) {
        this.atomicInteger = atomicInteger;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ",atomicInteger=" + atomicInteger.incrementAndGet());
    }
}

      

 

參考文章:

Java併發編程:volatile關鍵字解析

阿里面試:跟我死磕Synchronized底層實現,我滿分回答拿了Offer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章