java多線程4:volatile關鍵字

上文說到了 synchronized,那麼就不得不說下 volatile關鍵字了,它們兩者經常協同處理多線程的安全問題。

volatile保證可見性

那麼volatile的作用是什麼呢?

在jvm運行時刻內存的分配中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,

線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,

然後把堆內存變量的具體值load到線程本地內存中,建立一個變量副本,之後線程就不再和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,

在修改完之後的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。

 

 


read and load 從主存複製變量到當前工作內存
use and assign  執行代碼,改變共享變量值 
store and write 用工作內存數據刷新主存相關內容
其中use and assign 可以多次出現

注意這些操作並不是原子性,也就是 在read load之後,如果主內存count變量發生修改之後,線程工作內存中的值由於已經加載,

不會產生對應的變化,所以計算出來的結果會和預期不一樣,對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的。

如何解決緩存不一致的問題?

在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。

因爲CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),

從而使得只能有一個CPU能使用這個變量的內存。比如一個線程在執行 i = i +1,如果在執行這段代碼的過程中,在總線上發出了LCOK#鎖的信號,

那麼只有等待這段代碼完全執行完畢之後,其他CPU才能從變量i所在的內存讀取變量,然後進行相應的操作。

這樣就解決了緩存不一致的問題。但是上面的方式會有一個問題,由於在鎖住總線期間,其他CPU無法訪問內存,導致效率低下。

所以就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。

它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,

因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

看一個例子:

public class MyThread15 extends Thread
{
    private boolean isRunning = true;

    public boolean isRunning()
    {
        return isRunning;
    }

    public void setRunning(boolean isRunning)
    {
        this.isRunning = isRunning;
    }
    
    public void run()
    {
        System.out.println("進入run了");
        while (isRunning == true){}
        System.out.println("線程被停止了");
    }
}

  

@Test
    public void test15() throws InterruptedException {

    try{
        MyThread15 a = new MyThread15();
        a.start();
        Thread.sleep(1000);
        a.setRunning(false);
        System.out.println("已賦值爲false");
    }
    catch (InterruptedException e){
        e.printStackTrace();
    }
        a.join();
    }

  執行結果:

進入run了
已賦值爲false

  可以看待,明明已經將 isRunning設置爲false了,但是“線程被停止了”還是沒有被執行。

當我們給isRunning加上關鍵字volatile後,執行結果:

進入run了
已賦值爲false
線程被停止了

  可見volatile關鍵字可以保證共享變量在多線程之間的可見性。

 

volatile不保證原子性


下面看個demo,定義一個線程類,將count值++  10000次。

public class MyDomain16 {

    private volatile int count = 0;

    public MyDomain16() {
    }

    public void count() {
        for (int i=0; i<10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
        System.out.println(Thread.currentThread().getName() + ": count=" + count);
    }
} 
public class Mythread16 extends Thread {

    private MyDomain16 myDomain16;

    public Mythread16(MyDomain16 myDomain16) {
        this.myDomain16 = myDomain16;
    }

    public void run() {
        myDomain16.count();
    }
}

@Test
public void test16() throws InterruptedException {
    MyDomain16 myDomain16 = new MyDomain16();

    Mythread16 a = new Mythread16(myDomain16);
    Mythread16 b = new Mythread16(myDomain16);
    a.start();
    b.start();
    a.join();
    b.join();
}

  執行結果:

Thread-1: count=17913
Thread-0: count=17919

  

  可以看出兩個線程總共應該增加2萬次,但是count值並沒有等於20000,說明自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。

 

當我們給count方法加上synchronized之後

public class MyDomain16 {

    private volatile int count = 0;

    public MyDomain16() {
    }

    public synchronized void count() {
        for (int i=0; i<10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
        System.out.println(Thread.currentThread().getName() + ": count=" + count);
    }
}

  執行結果:

Thread-0: count=10000
Thread-1: count=20000

  

總結:

關鍵字synchronized和volatile進行一下比較:

1)關鍵字volatile是線程同步的輕量級實現,所以volatile性能肯定比synchronized要好,並且volatile只能修飾於變量,而synchronized可以修飾方法,以及代碼塊。

隨着JDK新版本的發佈,synchronized關鍵字在執行效率上得到很大提升,在開發中使用synchronized關鍵字的比率還是比較大的。

2)多線程訪問volatile不會發生阻塞,而synchronized會出現阻塞。

3)volatile能保證數據的可見性,但不能保證原子性;而synchronized可以保證原子性,也可以間接保證可見性,因爲它會將私有內存和公共內存中的數據做同步。

4)關鍵字volatile解決的是變量在多個線程之間的可見性;而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。

  線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。

 

參考文獻

1:《Java併發編程的藝術》

2:《Java多線程編程核心技術》

  

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