天天看點

java面試題:談談你對volatile的了解

  最近打算整理下Java面試中頻率比較高,相對比較難的一些面試題,感興趣的小夥伴可以關注下。

Volatile關鍵字

  ​

​volatile​

​​是Java虛拟機提供的​

​輕量級​

​​的同步機制.何為​

​輕量級​

​​呢,這要相對于​

​synchronized​

​來說。Volatile有如下三個特點。

volatile

保證可見性

不支援原子性

禁止指令重排序

  要搞清楚上面列舉的名詞​

​可見性​

​​ ​

​原子性​

​​ ​

​指令重排​

​的含義我們需要首先弄清楚JMM(Java記憶體模型是怎麼回事)

JMM

  JMM規定了記憶體主要劃分為​

​主記憶體​

​​和​

​工作記憶體​

​兩種。此處的主記憶體和工作記憶體跟JVM記憶體劃分(堆、棧、方法區)是在不同的層次上進行的,如果非要對應起來,主記憶體對應的是Java堆中的對象執行個體部分,工作記憶體對應的是棧中的部分區域,從更底層的來說,主記憶體對應的是硬體的實體記憶體,工作記憶體對應的是寄存器和高速緩存.

java面試題:談談你對volatile的了解

  JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主記憶體,對性能影響比較大,是以每條線程擁有各自的工作記憶體,工作記憶體中的變量是主記憶體中的一份​​

​拷貝​

​,線程對變量的讀取和寫入,直接在工作記憶體中操作,而不能直接去操作主記憶體中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作記憶體中變量,對其他線程是不可見的,會導緻線程不安全的問題。因為JMM制定了一套标準來保證開發者在編寫多線程程式的時候,能夠控制什麼時候記憶體會被同步給其他線程。

可見性

  各個線程對主記憶體中共享變量的操作都是各個線程各自拷貝到自己的工作記憶體進行操作後再寫回主記憶體中的。

  這就可能存在一個線程A修改了共享變量X的值但還未寫回主記憶體時,另一個線程B又對準記憶體中同一個共享變量X進行操作,但此時A線程工作記憶體中共享變量X對線程B來說并不是可見,這種工作記憶體與主記憶體同步存在延遲現象就造成了可見性問題。

  通過代碼來看下可見性的問題

package com.dpb.spring.aop.demo;

import java.util.concurrent.TimeUnit;

/**
 * 可見性問題分析
 */
public class VolatileDemo1 {
    public static void main(String[] args){
        final MyData myData = new MyData();
        // 開啟一個新的線程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "開始了...");
            try{TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
            // 在子線程中修改了變量的資訊  修改的本線程在工作記憶體中的資料
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "更新後的資料是:"+myData.number);
        },"BBB").start();
        // 因為在其他線程中修改的資訊主線程的工作記憶體中的資料并沒有改變是以此時number還是為0
        while(myData.number == 0){
            // 會一直卡在此處
            //System.out.println("1111");
        }
        System.out.println(Thread.currentThread().getName()+"\t number =  " + myData.number);
    }
}

class MyData{
    // 沒有用volatile來修飾
    int number = 0;

    public void addTo60(){
        this.number = 60;
    }

}      

效果如下:

java面試題:談談你對volatile的了解

  通過​

​volatile​

​來解決此問題

java面試題:談談你對volatile的了解
java面試題:談談你對volatile的了解

  我們可以發現當變量被​

​volatile​

​修飾的時候,在子線程的工作記憶體中的變量被修改後其他線程中對應的變量是可以立馬知道的。這就是我們講的可見性

原子性

  原子性是​

​不可分割​

​​,​

​完整性​

​​,也即某個線程正在做某個具體業務時,中間不可以被加塞或者分割,需要整體完成,要麼同時成功,要麼同時失敗.

  volatile是​​

​不支援​

​原子性的,接下來我們可以驗證下。

package com.dpb.spring.aop.demo;

import java.util.concurrent.TimeUnit;

/**
 * 可見性問題分析
 */
public class VolatileDemo2 {
    public static void main(String[] args){
        final MyData2 myData = new MyData2();
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    myData.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        // 等待子線程執行完成
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        // 在主線程中擷取統計的資訊值
        System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number);
    }
}

class MyData2{
   // 操作的變量被volatile修飾了
    volatile int number = 0;

    public void addPlusPlus(){
        number++;
    }

}      

執行的效果

java面試題:談談你對volatile的了解

  根據正常的邏輯在開啟的20個子線程,每個執行1000遍累加,得到的結果應該是20000,但是我們發現運作的結果大機率會比我們期望的要小,而且變量也已經被volatile修飾了。說明并沒有滿足我們要求的原子性。這種情況下我們要保證操作的原子性,我們有兩個選擇

  1. 通過synchronized來實作
  2. 通過​

    ​JUC​

    ​​下的​

    ​AtomicInteger​

    ​來實作

  synchronized的實作是重量級的,影響并發的效率,是以我們通過AtomicInteger來實作。

package com.dpb.spring.aop.demo;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 可見性問題分析
 */
public class VolatileDemo2 {
    public static void main(String[] args){
        final MyData2 myData = new MyData2();
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    myData.addPlusPlus();
                    myData.addAtomicPlus();
                }
            },String.valueOf(i)).start();
        }
        // 等待子線程執行完成
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        // 在主線程中擷取統計的資訊值
        System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.atomicInteger.get());
    }
}

class MyData2{
   // 操作的變量被volatile修飾了
    volatile int number = 0;
    // AtomicInteger 來保證操作的原子性
    AtomicInteger atomicInteger = new AtomicInteger();

    public  void addPlusPlus(){
        number++;
    }

    public void addAtomicPlus(){
        atomicInteger.getAndIncrement();
    }

}      

效果:

java面試題:談談你對volatile的了解

​​

​注意​

​​:通過效果發現​

​AtomicInteger​

​​在多線程環境下處理的資料和我們期望的結果是一緻的都是​

​20000​

​.說明實作的操作的原子性。

有序性

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

java面試題:談談你對volatile的了解
  • 單線程環境裡面確定程式最終執行結果和代碼順序執行的結果一緻。
  • 處理器在進行重排序時必須考慮指令之間的​

    ​資料依賴性​

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

案例代碼

package com.dpb.spring.aop.demo;

public class SortDemo {
    int a = 0;
    boolean flag = false;

    public void fun1(){
        a = 1;  // 語句1
        flag = true; // 語句2
    }

    public void fun2(){
        if(flag){
            a = a + 5; // 語句3
            System.out.println("a = " + a );
        }
    }
}      

​注意:​

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

指令重排小結:

  volatile實作禁止指令重排優化,進而避免多線程環境下程式出現亂序執行的現象。

先了解一個概念,​​

​記憶體屏障​

​​又稱​

​記憶體栅欄​

​,是一個CPU指令,它的作用有兩個:

  1. 是保證特定操作的執行順序
  2. 是保證某些變量的記憶體可見性(利用該特性實作volatile的記憶體可見性)
  1. 工作記憶體和主記憶體同步延遲現象導緻的​

    ​可見性問題​

    ​,可以使用synchronized或volatile關鍵字解決,他們都可以使一個線程修改後的變量立即對其他線程可見。
  2. 對于指令重排導緻的​

    ​可見性問題​

    ​和​

    ​有序性問題​

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