對象的可見性 - volatile篇

做者:湯圓java

我的博客:javalover.cc編程

前言

官人們好啊,我是湯圓,今天給你們帶來的是《對象的可見性 - volatile篇》,但願有所幫助,謝謝性能優化

文章若是有誤,但願你們能夠指出,真心感謝

簡介

當一個線程修改了某個共享變量時(非局部變量,全部線程均可以訪問獲得),其餘線程老是能立馬讀到最新值,這時咱們就說這個變量是具備可見性的多線程

若是是單線程,那麼可見性是毋庸置疑的,確定改了就能看到(直腸子,有啥說啥,你們都能看到)併發

可是若是是多線程,那麼可見性就須要經過一些手段來維持了,好比加鎖或者volatile修飾符(花花腸子,各類套路讓人措手不及)高併發

PS:實際上,沒有真正的直腸子,據科學研究代表,人的腸子長達8米左右(~身高的5倍)

目錄

  1. 單線程和多線程中的可見性對比
  2. volatile修飾符
  3. 指令重排序
  4. volatile和加鎖的區別

正文

1. 單線程和多線程中的可見性對比

這裏咱們舉兩個例子來看下,來了解什麼是可見性問題性能

下面是一個單線程的例子,其中有一個共享變量優化

public class SignleThreadVisibilityDemo {
    // 共享變量
    private int number;
    public void setNumber(int number){
        this.number = number;
    }
    public int getNumber(){
        return this.number;
    }
    public static void main(String[] args) {
        SignleThreadVisibilityDemo demo = new SignleThreadVisibilityDemo();
        System.out.println(demo.getNumber());
        demo.setNumber(10);
        System.out.println(demo.getNumber());
    }
}

輸出以下:能夠看到,第一次共享變量number爲初始值0,可是調用setNumber(10)以後,再讀取就變成了10this

0
10

改了就能看到,若是多線程也有這麼簡單,那多好(來自菜鳥的心裏獨白)。spa

下面咱們看一個多線程的例子,仍是那個共享變量

package com.jalon.concurrent.chapter3;

/**
 * <p>
 *  可見性:多線程的可見性問題
 * </p>
 *
 * @author: JavaLover
 * @time: 2021/4/27
 */
public class MultiThreadVisibilityDemo {
    // 共享變量
    private int number;
    public static void main(String[] args) throws InterruptedException {
        MultiThreadVisibilityDemo demo = new MultiThreadVisibilityDemo();
        new Thread(()->{
              // 這裏咱們作個假死循環,只有沒給number賦值(初始化除外),就一直循環
            while (0==demo.number);
            System.out.println(demo.number);
        }).start();
        Thread.sleep(1000);
        // 168不是身高,只是個比較吉利的數字
        demo.setNumber(168);
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

}

輸出以下:

你沒看錯,就是輸出爲空,並且程序還在一直運行(沒有試過,若是不關機,會不會有輸出number的那一天)

這時就出現了可見性問題,即主線程改了共享變量number,而子線程卻看不到

緣由是什麼呢?

咱們用圖來講話吧,會輕鬆點

可見性問題

步驟以下:

  1. 子線程讀取number到本身的棧中,備份
  2. 主線程讀取number,修改,寫入,同步到內存
  3. 子線程此時沒有意識到number的改變,仍是讀本身棧中的備份ready(多是各類性能優化的緣由)
那要怎麼解決呢?

加鎖或者volatile修飾符,這裏咱們加volatile

修改後的代碼以下:

public class MultiThreadVisibilityDemo {
    // 共享變量,加了volatile修飾符,此時number不會備份到其餘線程,只會存在共享的堆內存中
    private volatile int number;
    public static void main(String[] args) throws InterruptedException {
        MultiThreadVisibilityDemo demo = new MultiThreadVisibilityDemo();
        new Thread(()->{
            while (0==demo.number);
            System.out.println(demo.number);
        }).start();
        Thread.sleep(1000);
        // 168不是身高,只是個比較吉利的數字
        demo.setNumber(168);
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

}

輸出以下:

168

能夠看到,跟咱們預期的同樣,子線程能夠看到主線程作的修改

下面就讓咱們一塊兒來探索volatile的小世界吧

2. volatile修飾符

volatile是一種比加鎖稍弱的同步機制,它和加鎖最大的區別就是,它不能保證原子性,可是它輕量啊

咱們先把上面那個例子說完;

咱們加了volatile修飾符後,子線程就能夠看到主線程作的修改,那麼volatile到底作了什麼呢?

其實咱們能夠把volatile看作一個標誌,若是虛擬機看到這個標誌,就會認爲被它修飾的變量是易變的,不穩定的,隨時可能被某個線程修改;

此時虛擬機就不會對與這個變量相關的指令進行重排序(下面會講到),並且還會將這個變量的改變實時通知到各個線程(可見性)

用圖說話的話,就是下面這個樣子:

可見性問題-volatile

能夠看到,線程中的number備份都不須要了,每次須要number的時候,都直接去堆內存中讀取,這樣就保證了數據的可見性

3. 指令重排序

指令重排序指的是,虛擬機有時候爲了優化性能,會把某些指令的執行順序進行調整,前提是指令的依賴關係不能被破壞(好比int a = 10; int b = a;此時就不會重排序)

下面咱們看下可能會重排序的代碼:

public class ReorderDemo {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int m = a + b;
        int c = 1;
        int d = 2;
        int n = c - d;
    }
}

這裏咱們要了解一個底層知識,就是每一條語句的執行,在底層系統都是分好幾步走的(好比第一步,第二步,第三步等等,這裏咱們就不涉及那些彙編知識了,你們感興趣能夠參考看下《實戰Java高併發》1.5.4);

如今讓咱們回到上面這個例子,依賴關係以下:

依賴關係

能夠看到,他們三三成堆,互不依賴,此時若是發生了重排序,那麼就有可能排成下面這個樣子

重排序

(上圖只是從代碼層面進行的效果演示,實際上指令的重排序比這個細節不少,這裏主要了解重排序的思想先)

因爲m=a+b須要依賴a和b的值,因此當指令執行到m=a+b的add環節時,若是b還沒準備好,那麼m=a+b就須要等待b,後面的指令也會等待;

可是若是重排序,把m=a+b放到後面,那麼就能夠利用add等待的這個空檔期,去準備c和d;

這樣就減小了等待時間,提高了性能(感受有點像上學時候學的C,習慣性地先定義變量一大堆,而後再編寫代碼)

4. volatile和加鎖的區別

區別以下

加鎖 volatile
原子性
可見性
有序性

上面所說的有序性指的就是禁止指令的重排序,從而使得多線程中不會出現亂序的問題;

咱們能夠看到,加鎖和volatile最大的區別就是原子性;

主要是由於volatile只是針對某個變量進行修飾,因此就有點像原子變量的複合操做(雖然原子變量自己是原子操做,可是多個原子變量放到一塊兒,就沒法保證了)

總結

  1. 可見性在單線程中沒問題,可是多線程會有問題
  2. volatile是一種比加鎖輕量級的同步機制,能夠保證變量的可見性和有序性(禁止重排序)
  3. 指令重排序:有時虛擬機爲了優化性能,會在運行時把相互沒有依賴的代碼順序從新排序,以此來減小指令的等待時間,提升效率
  4. 加鎖和volatile的區別:加鎖能夠保證原子性,volatile不能夠

參考內容:

  • 《Java併發編程實戰》
  • 《實戰Java高併發》

後記

最後,感謝你們的觀看,謝謝

原創不易,期待官人們的三連喲

相關文章
相關標籤/搜索