溫馨提示×

溫馨提示×

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

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

java多線程基礎(chǔ)知識整理

發(fā)布時間:2021-08-13 19:09:28 來源:億速云 閱讀:156 作者:chen 欄目:開發(fā)技術(shù)

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

目錄
  • Java內(nèi)存模型

  • 主內(nèi)存和工作內(nèi)存的交互命令

  • 內(nèi)存模型的原子性

  • 內(nèi)存模型的可見性

  • 內(nèi)存模型的有序性

  • 指令重排優(yōu)化的底層原理

  • valatile原理

  • volatile與加鎖的區(qū)別

  • 先行發(fā)生原則

  • 線程的三種實現(xiàn)方式


Java內(nèi)存模型

  • Java內(nèi)存模型與Java內(nèi)存結(jié)構(gòu)不同,Java內(nèi)存結(jié)構(gòu)指的是jvm內(nèi)存分區(qū)。Java內(nèi)存模型描述的是多線程環(huán)境下原子性,可見性,有序性的規(guī)則和保障。

  • Java內(nèi)存模型提供了主內(nèi)存和工作內(nèi)存兩種抽象,主內(nèi)存指的是共享區(qū)域 ,工作內(nèi)存指的是線程私有工作空間。

  • 當(dāng)一個線程訪問共享數(shù)據(jù)時,需要先將共享數(shù)據(jù)復(fù)制一份副本到線程的工作內(nèi)存(類比操作系統(tǒng)中的高速緩存),然后在工作內(nèi)存進(jìn)行操作,最后再把工作內(nèi)存數(shù)據(jù)覆蓋到主內(nèi)存。主內(nèi)存和工作內(nèi)存交互通過特定指令完成。

  • 如下為并發(fā)內(nèi)存模型圖

java多線程基礎(chǔ)知識整理

多線程環(huán)境下原子性,可見性,有序性分別指的是

  • 原子性:程序執(zhí)行不會受到線程上下文切換的影響。

  • 可見性:程序執(zhí)行不會受到CPU緩存影響。

  • 有序性:程序執(zhí)行不會受到CPU指令并行優(yōu)化的影響。

主內(nèi)存和工作內(nèi)存的交互命令

  • lock:把主內(nèi)存的一個變量標(biāo)記為一個線程鎖定狀態(tài)。

  • unlock:把主內(nèi)存中處于鎖定狀態(tài)的變量釋放出來。

  • read:把主內(nèi)存的變量讀取到線程工作內(nèi)存。

  • load:把工作內(nèi)存的值放入工作內(nèi)存變量副本中。

  • use:把工作內(nèi)存變量的值傳遞給執(zhí)行引擎。

  • assign:把執(zhí)行引擎接收到的值賦值給工作內(nèi)存變量。

  • store:把工作內(nèi)存的值傳送到主內(nèi)存中。

  • write:把工作內(nèi)存的值寫入到工作內(nèi)存變量。

內(nèi)存模型的原子性

Java內(nèi)存模型只保證store和write兩個命令按順序執(zhí)行,但不保證連續(xù)執(zhí)行,因此多個線程同時寫入共享變量可能出現(xiàn)線程安全問題。

諸如i++的操作,首先將主存中的變量i的值拷貝一份拿到線程的本地內(nèi)存,在本地內(nèi)存進(jìn)行自增操作,然后將新的i值寫回主存。
但是涉及到多線程環(huán)境下的線程上下文切換就會出現(xiàn)問題,可能線程1將i值拿來進(jìn)行自增操作,然后還來不及寫回主存,時間片用完,輪到線程2執(zhí)行,線程2對i進(jìn)行自減操作,然后輪到線程1時,線程1將上一次的值寫回內(nèi)存,就會將線程2上一步的計算結(jié)果覆蓋,就會產(chǎn)生錯誤的結(jié)果。

通過多線程的學(xué)習(xí)我們知道,對共享數(shù)據(jù)加鎖可以保證操作的原子性,相當(dāng)于i++操作對應(yīng)底層命令是原子化綁定的,這樣就不會出現(xiàn)線程安全問題,但是會導(dǎo)致程序性能降低。

內(nèi)存模型的可見性

  • 對于頻繁從主存取值的操作,JIT可能會將其進(jìn)行優(yōu)化,以后每次操作不從主存取值,而是從CPU緩存中取值。一旦線程1每次從寄存器取值,那么此時主存中變量值的變化對于線程1來說就是不可見的。

  • 如下,子線程是無法感知主存中flag的修改的,子線程就無法停止。

    public class Test {
        static boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
            //3秒后線程無法停止
            new Thread(()->{
                while(flag){
                }
            }).start();
            Thread.sleep(3000);
            System.out.println("flag = false");
            flag =false;
        }
    }
  • 有兩種方法可以保證主存中數(shù)據(jù)的可見性,方法1是加鎖。加鎖既可以保證原子性,又可以保證可見性。

    public class Test {
        static boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
                    new Thread(()->{
                        while(flag){
                            synchronized (Test.class){}
                        }
                    }).start();
                    Thread.sleep(3000);
                    System.out.println("flag = false");
                    flag =false;
                }
    }
  • 還有一種方法是使用volatile關(guān)鍵字,它可以保證當(dāng)前線程對共享變量的修改對另一個線程是一直可見的。volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但是volatile關(guān)鍵字只能保證可見性,不能保證原子性。volatile適用于一個線程寫多個線程讀的應(yīng)用場景,保證各個線程可以實時感知到其他線程更新的數(shù)據(jù)。

    public class Test {
        static volatile boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
                    new Thread(()->{
                        while(flag){
                        }
                    }).start();
                    Thread.sleep(3000);
                    System.out.println("flag = false");
                    flag =false;
                }
    }
  • 對于多線程同時操作共享變量的情況,使用volatile關(guān)鍵字依然會出現(xiàn)線程安全問題,因為原子性無法保證。

public class Test {
    static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a++;
            }
        }).start();
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a--;
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(a); //不能保證a為0
    }
}

內(nèi)存模型的有序性

有序性是指在單線程環(huán)境中, 程序是按序依次執(zhí)行的。而在多線程環(huán)境中, 程序的執(zhí)行可能因為指令重排而出現(xiàn)亂序。

指令重排是指在程序執(zhí)行過程中, 為了性能考慮, 編譯器和CPU可能會對指令重新排序。這種排序(比如兩個變量的定義順序)不會影響單線程的結(jié)果,但是會對多線程程序產(chǎn)生影響。

比如 a=1 b=2兩條語句就可能發(fā)生指令重排。而 a=1,b=a+1 不會發(fā)生指令重排。

示例:線程1執(zhí)行f1方法,線程2執(zhí)行f2方法。兩個線程同時執(zhí)行,可能發(fā)生如下結(jié)果: f1中發(fā)生指令重排 flag=true先執(zhí)行,a=1后執(zhí)行。線程1先執(zhí)行flag=true,然后輪到線程2執(zhí)行,此時flag為true,執(zhí)行if語句,i=1。這就是指令重排造成的程序錯亂。

class Test{
    int a = 0;
    boolean flag = false;
    public void f1() {
        a = 1;                   
        flag = true;           
    }
    public void f2() {
        if (flag) {                
            int i =  a +1;      
        }
    }
}

可以用volatile修飾flag來禁用指令重排達(dá)到有序性。

加鎖也可以避免指令重排帶來的混亂,但是本身并沒有禁止指令重排,因為保證了原子性,所以即使指令重排在同步代碼塊中依然相當(dāng)于單線程執(zhí)行,也不會有邏輯上的錯誤。

指令重排優(yōu)化的底層原理

一個指令的執(zhí)行被分成:取指、譯碼、訪存、執(zhí)行、寫回 5個階段。然后,多條指令可以同時存在于流水線中,同時被執(zhí)行。
指令流水線并不是串行的,并不會因為一個耗時很長的指令在“執(zhí)行”階段呆很長時間,而導(dǎo)致后續(xù)的指令阻塞。相反,流水線是并行的,多個指令可以同時處于同一個階段,只要CPU內(nèi)部相應(yīng)的處理部件未被占滿即可。

比如,依次有兩條指令a和b需要執(zhí)行,如果是串行執(zhí)行,它們的執(zhí)行過程如下

指令a                          指令b
階段1 階段2 階段3 階段4 階段5    階段1 階段2 階段3 階段4 階段5

但是,假如階段2耗時很長,使用串行的方式就無法在一個階段阻塞的時候去執(zhí)行其他階段。

如下就是流水線的方式來執(zhí)行,當(dāng)指令a的階段2阻塞時,完全可以去執(zhí)行指令b的階段1,這樣就提高了程序執(zhí)行效率,最大程度利用CPU各個部件。

指令a                         
階段1 階段2 階段3 階段4 階段5   
指令b
      階段1 階段2 階段3 階段4 階段5

因此指令重排就是對于一個線程中的多個指令,可以在不影響單線程執(zhí)行結(jié)果的前提下,將某些指令的各個階段進(jìn)行重排序和組合,實現(xiàn)指令級并行。

valatile原理

如下,假設(shè)對變量a用valatile關(guān)鍵字修飾。

valatile int a = 0;

那么,對變量a的寫指令之后都會插入寫屏障,對變量a的讀指令之前都會插入讀屏障。

a++;
//寫屏障
//讀屏障
int b = a;

寫屏障會保證寫屏障之前的所有對共享數(shù)據(jù)的改動都會同步到主存中。讀屏障會保證讀屏障之后對共享數(shù)據(jù)的讀取操作都會到主存去讀取。這樣就保證了,每次對valatile變量的修改對其他線程始終是可見的,從而保證了可見性。

另外,寫屏障會保證寫屏障之前的指令不會被排到寫屏障后面。讀屏障會保證讀屏障之后的代碼不會排到讀屏障前面。這樣就保證了有序性。

如下,由于寫屏障的存在,int b=1;語句只能排在 a++前面,不能顛倒順序。

int b=1;
a++;
//寫屏障

volatile與加鎖的區(qū)別

volatile只能保證可見性和有序性,不能保證原子性,加鎖既可以保證可見性 原子性 有序性都可以保證。

volatile只適用于一個線程寫,多個線程讀的情況,對于多個線程寫的情況,必須要加鎖。

加鎖相對于volatile是更加重量級的操作,所以一般能用volatile解決的問題就不要加鎖。

先行發(fā)生原則

先行發(fā)生是Java內(nèi)存模型中定義的兩項操作之間的偏序關(guān)系。如果說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響被操作B察覺。

先行發(fā)生原則–是判斷是否存在數(shù)據(jù)競爭、線程是否安全的主要依據(jù)。先行發(fā)生原則主要用來解決可見性問題的。

如下代碼

//以下操作在線程A中執(zhí)行
i = 1;
//以下操作在線程B中執(zhí)行
j = i;
//以下操作在線程C中執(zhí)行
i = 2

如果A先行發(fā)生于B,B先行發(fā)生于C,那么必然j的值為1。如果A先行發(fā)生于B,B和C沒有先行發(fā)生關(guān)系,那么j的值可能為1也可能為2。

Java內(nèi)存模型存在一些天然的先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系不需要任何的同步操作,就可以保證其線程安全。

1、程序次序規(guī)則。在一個線程內(nèi),書寫在前面的代碼先行發(fā)生于后面的。確切地說應(yīng)該是,按照程序的控制流順序,因為存在一些分支結(jié)構(gòu)。

2、Volatile變量規(guī)則。對一個volatile修飾的變量,對他的寫操作先行發(fā)生于讀操作。

3、線程啟動規(guī)則。Thread對象的start()方法先行發(fā)生于此線程的每一個動作。

4、線程終止規(guī)則。線程的所有操作都先行發(fā)生于對此線程的終止檢測。

5、線程中斷規(guī)則。對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼所檢測到的中斷事件。

6、對象終止規(guī)則。一個對象的初始化完成(構(gòu)造函數(shù)之行結(jié)束)先行發(fā)生于發(fā)的finilize()方法的開始。

7、傳遞性。A先行發(fā)生B,B先行發(fā)生C,那么,A先行發(fā)生C。

8、管程鎖定規(guī)則。一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。

線程的三種實現(xiàn)方式

  • 使用內(nèi)核線程實現(xiàn)

  • 內(nèi)核線程就是直接由操作系統(tǒng)內(nèi)核支持的線程,通過內(nèi)核完成線程的切換。

  • 通過線程調(diào)度器來負(fù)責(zé)線程調(diào)度,即將線程任務(wù)分配到指定處理器。

  • 在用戶態(tài),每個內(nèi)核級線程會一 一對應(yīng)一個輕量級進(jìn)程,就是通常所說的用戶級線程,多個用戶級線程可以組成一個用戶進(jìn)程。

  • 如下所示:p進(jìn)程 LWP用戶線程 KLT內(nèi)核線程 Thread Scheduler 線程調(diào)度器

java多線程基礎(chǔ)知識整理

  • 由于內(nèi)核線程的支持,每個用戶線程都是獨立調(diào)度單位,即使有一個用戶線程阻塞了,也不會影響當(dāng)前進(jìn)程其他線程執(zhí)行。但是用戶線程切換 創(chuàng)建 終止都要內(nèi)核支持,內(nèi)核與用戶態(tài)切換代價較高。

  • Java就是使用內(nèi)核線程實現(xiàn)的,無論是windows還是linux都是基于內(nèi)核線程實現(xiàn)的。

  • 使用用戶線程實現(xiàn)

  • 操作系統(tǒng)內(nèi)核只能感知到用戶進(jìn)程,用戶進(jìn)程為操作系統(tǒng)內(nèi)核的基本調(diào)度單位。

  • 基于用戶進(jìn)程實現(xiàn)的用戶線程,線程的創(chuàng)建 切換 銷毀都是進(jìn)程自己管理,與內(nèi)核沒有關(guān)系。因為操作系統(tǒng)只能把處理器資源分配到進(jìn)程,那么線程的運行 阻塞 生命周期管理都要用戶進(jìn)程自己來實現(xiàn)。

  • 內(nèi)核不參與線程調(diào)度,因此線程的上下文切換開銷比較小,但是實現(xiàn)起來非常復(fù)雜,而且當(dāng)一個用戶級線程阻塞整個進(jìn)程都會阻塞,并發(fā)度不高。

java多線程基礎(chǔ)知識整理

  • 混合模式實現(xiàn)

  • 用戶線程和內(nèi)核線程使用M對N的映射來實現(xiàn),兼顧兩者的優(yōu)點。

java多線程基礎(chǔ)知識整理

“java多線程基礎(chǔ)知識整理”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

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

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

AI