溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

單線程和多線程中的可見性的區(qū)別是什么

發(fā)布時(shí)間:2021-10-12 11:22:48 來源:億速云 閱讀:96 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“單線程和多線程中的可見性的區(qū)別是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

簡(jiǎn)介

當(dāng)一個(gè)線程修改了某個(gè)共享變量時(shí)(非局部變量,所有線程都可以訪問得到),其他線程總是能立馬讀到最新值,這時(shí)我們就說這個(gè)變量是具有可見性的

如果是單線程,那么可見性是毋庸置疑的,肯定改了就能看到(直腸子,有啥說啥,大家都能看到)

但是如果是多線程,那么可見性就需要通過一些手段來維持了,比如加鎖或者volatile修飾符(花花腸子,各種套路讓人措手不及)

PS:實(shí)際上,沒有真正的直腸子,據(jù)科學(xué)研究表明,人的腸子長(zhǎng)達(dá)8米左右(~身高的5倍)

目錄

  1. 單線程和多線程中的可見性對(duì)比

  2. volatile修飾符

  3. 指令重排序

  4. volatile和加鎖的區(qū)別

正文

1. 單線程和多線程中的可見性對(duì)比

這里我們舉兩個(gè)例子來看下,來了解什么是可見性問題

下面是一個(gè)單線程的例子,其中有一個(gè)共享變量

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,但是調(diào)用setNumber(10)之后,再讀取就變成了10

0
10

改了就能看到,如果多線程也有這么簡(jiǎn)單,那多好(來自菜鳥的內(nèi)心獨(dú)白)。

下面我們看一個(gè)多線程的例子,還是那個(gè)共享變量

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(()->{
          	// 這里我們做個(gè)假死循環(huán),只有沒給number賦值(初始化除外),就一直循環(huán)
            while (0==demo.number);
            System.out.println(demo.number);
        }).start();
        Thread.sleep(1000);
        // 168不是身高,只是個(gè)比較吉利的數(shù)字
        demo.setNumber(168);
    }

    public int getNumber() {
        return number;
    }

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

}

輸出如下:


你沒看錯(cuò),就是輸出為空,而且程序還在一直運(yùn)行(沒有試過,如果不關(guān)機(jī),會(huì)不會(huì)有輸出number的那一天)

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

原因是什么呢?

我們用圖來說話吧,會(huì)輕松點(diǎn)

單線程和多線程中的可見性的區(qū)別是什么

步驟如下:

  1. 子線程讀取number到自己的棧中,備份

  2. 主線程讀取number,修改,寫入,同步到內(nèi)存

  3. 子線程此時(shí)沒有意識(shí)到number的改變,還是讀自己棧中的備份ready(可能是各種性能優(yōu)化的原因)

那要怎么解決呢?

加鎖或者volatile修飾符,這里我們加volatile

修改后的代碼如下:

public class MultiThreadVisibilityDemo {
    // 共享變量,加了volatile修飾符,此時(shí)number不會(huì)備份到其他線程,只會(huì)存在共享的堆內(nèi)存中
    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不是身高,只是個(gè)比較吉利的數(shù)字
        demo.setNumber(168);
    }

    public int getNumber() {
        return number;
    }

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

}

輸出如下:

168

可以看到,跟我們預(yù)期的一樣,子線程可以看到主線程做的修改

下面就讓我們一起來探索volatile的小世界吧

2. volatile修飾符

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

我們先把上面那個(gè)例子說完;

我們加了volatile修飾符后,子線程就可以看到主線程做的修改,那么volatile到底做了什么呢?

其實(shí)我們可以把volatile看做一個(gè)標(biāo)志,如果虛擬機(jī)看到這個(gè)標(biāo)志,就會(huì)認(rèn)為被它修飾的變量是易變的,不穩(wěn)定的,隨時(shí)可能被某個(gè)線程修改;

此時(shí)虛擬機(jī)就不會(huì)對(duì)與這個(gè)變量相關(guān)的指令進(jìn)行重排序(下面會(huì)講到),而且還會(huì)將這個(gè)變量的改變實(shí)時(shí)通知到各個(gè)線程(可見性)

用圖說話的話,就是下面這個(gè)樣子:

單線程和多線程中的可見性的區(qū)別是什么

可以看到,線程中的number備份都不需要了,每次需要number的時(shí)候,都直接去堆內(nèi)存中讀取,這樣就保證了數(shù)據(jù)的可見性

3. 指令重排序

指令重排序指的是,虛擬機(jī)有時(shí)候?yàn)榱藘?yōu)化性能,會(huì)把某些指令的執(zhí)行順序進(jìn)行調(diào)整,前提是指令的依賴關(guān)系不能被破壞(比如int a = 10; int b = a;此時(shí)就不會(huì)重排序)

下面我們看下可能會(huì)重排序的代碼:

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;
    }
}

這里我們要了解一個(gè)底層知識(shí),就是每一條語句的執(zhí)行,在底層系統(tǒng)都是分好幾步走的(比如第一步,第二步,第三步等等,這里我們就不涉及那些匯編知識(shí)了,大家感興趣可以參考看下《實(shí)戰(zhàn)Java高并發(fā)》1.5.4);

現(xiàn)在讓我們回到上面這個(gè)例子,依賴關(guān)系如下:

單線程和多線程中的可見性的區(qū)別是什么

可以看到,他們?nèi)啥?,互不依賴,此時(shí)如果發(fā)生了重排序,那么就有可能排成下面這個(gè)樣子

單線程和多線程中的可見性的區(qū)別是什么

(上圖只是從代碼層面進(jìn)行的效果演示,實(shí)際上指令的重排序比這個(gè)細(xì)節(jié)很多,這里主要了解重排序的思想先)

由于m=a+b需要依賴a和b的值,所以當(dāng)指令執(zhí)行到m=a+b的add環(huán)節(jié)時(shí),如果b還沒準(zhǔn)備好,那么m=a+b就需要等待b,后面的指令也會(huì)等待;

但是如果重排序,把m=a+b放到后面,那么就可以利用add等待的這個(gè)空檔期,去準(zhǔn)備c和d;

這樣就減少了等待時(shí)間,提升了性能(感覺有點(diǎn)像上學(xué)時(shí)候?qū)W的C,習(xí)慣性地先定義變量一大堆,然后再編寫代碼)

4. volatile和加鎖的區(qū)別

區(qū)別如下


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

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

我們可以看到,加鎖和volatile最大的區(qū)別就是原子性;

主要是因?yàn)関olatile只是針對(duì)某個(gè)變量進(jìn)行修飾,所以就有點(diǎn)像原子變量的復(fù)合操作(雖然原子變量本身是原子操作,但是多個(gè)原子變量放到一起,就無法保證了)

“單線程和多線程中的可見性的區(qū)別是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI