中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

java多線程基礎知識整理

發布時間:2021-08-13 19:09:28 來源:億速云 閱讀:163 作者:chen 欄目:開發技術

本篇內容介紹了“java多線程基礎知識整理”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

目錄
  • Java內存模型

  • 主內存和工作內存的交互命令

  • 內存模型的原子性

  • 內存模型的可見性

  • 內存模型的有序性

  • 指令重排優化的底層原理

  • valatile原理

  • volatile與加鎖的區別

  • 先行發生原則

  • 線程的三種實現方式


Java內存模型

  • Java內存模型與Java內存結構不同,Java內存結構指的是jvm內存分區。Java內存模型描述的是多線程環境下原子性,可見性,有序性的規則和保障。

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

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

  • 如下為并發內存模型圖

java多線程基礎知識整理

多線程環境下原子性,可見性,有序性分別指的是

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

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

  • 有序性:程序執行不會受到CPU指令并行優化的影響。

主內存和工作內存的交互命令

  • lock:把主內存的一個變量標記為一個線程鎖定狀態。

  • unlock:把主內存中處于鎖定狀態的變量釋放出來。

  • read:把主內存的變量讀取到線程工作內存。

  • load:把工作內存的值放入工作內存變量副本中。

  • use:把工作內存變量的值傳遞給執行引擎。

  • assign:把執行引擎接收到的值賦值給工作內存變量。

  • store:把工作內存的值傳送到主內存中。

  • write:把工作內存的值寫入到工作內存變量。

內存模型的原子性

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

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

通過多線程的學習我們知道,對共享數據加鎖可以保證操作的原子性,相當于i++操作對應底層命令是原子化綁定的,這樣就不會出現線程安全問題,但是會導致程序性能降低。

內存模型的可見性

  • 對于頻繁從主存取值的操作,JIT可能會將其進行優化,以后每次操作不從主存取值,而是從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;
        }
    }
  • 有兩種方法可以保證主存中數據的可見性,方法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關鍵字,它可以保證當前線程對共享變量的修改對另一個線程是一直可見的。volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但是volatile關鍵字只能保證可見性,不能保證原子性。volatile適用于一個線程寫多個線程讀的應用場景,保證各個線程可以實時感知到其他線程更新的數據。

    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關鍵字依然會出現線程安全問題,因為原子性無法保證。

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

內存模型的有序性

有序性是指在單線程環境中, 程序是按序依次執行的。而在多線程環境中, 程序的執行可能因為指令重排而出現亂序。

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

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

示例:線程1執行f1方法,線程2執行f2方法。兩個線程同時執行,可能發生如下結果: f1中發生指令重排 flag=true先執行,a=1后執行。線程1先執行flag=true,然后輪到線程2執行,此時flag為true,執行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來禁用指令重排達到有序性。

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

指令重排優化的底層原理

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

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

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

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

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

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

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

valatile原理

如下,假設對變量a用valatile關鍵字修飾。

valatile int a = 0;

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

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

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

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

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

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

volatile與加鎖的區別

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

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

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

先行發生原則

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

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

如下代碼

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

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

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

1、程序次序規則。在一個線程內,書寫在前面的代碼先行發生于后面的。確切地說應該是,按照程序的控制流順序,因為存在一些分支結構。

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

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

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

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

6、對象終止規則。一個對象的初始化完成(構造函數之行結束)先行發生于發的finilize()方法的開始。

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

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

線程的三種實現方式

  • 使用內核線程實現

  • 內核線程就是直接由操作系統內核支持的線程,通過內核完成線程的切換。

  • 通過線程調度器來負責線程調度,即將線程任務分配到指定處理器。

  • 在用戶態,每個內核級線程會一 一對應一個輕量級進程,就是通常所說的用戶級線程,多個用戶級線程可以組成一個用戶進程。

  • 如下所示:p進程 LWP用戶線程 KLT內核線程 Thread Scheduler 線程調度器

java多線程基礎知識整理

  • 由于內核線程的支持,每個用戶線程都是獨立調度單位,即使有一個用戶線程阻塞了,也不會影響當前進程其他線程執行。但是用戶線程切換 創建 終止都要內核支持,內核與用戶態切換代價較高。

  • Java就是使用內核線程實現的,無論是windows還是linux都是基于內核線程實現的。

  • 使用用戶線程實現

  • 操作系統內核只能感知到用戶進程,用戶進程為操作系統內核的基本調度單位。

  • 基于用戶進程實現的用戶線程,線程的創建 切換 銷毀都是進程自己管理,與內核沒有關系。因為操作系統只能把處理器資源分配到進程,那么線程的運行 阻塞 生命周期管理都要用戶進程自己來實現。

  • 內核不參與線程調度,因此線程的上下文切換開銷比較小,但是實現起來非常復雜,而且當一個用戶級線程阻塞整個進程都會阻塞,并發度不高。

java多線程基礎知識整理

  • 混合模式實現

  • 用戶線程和內核線程使用M對N的映射來實現,兼顧兩者的優點。

java多線程基礎知識整理

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

咸丰县| 德清县| 龙海市| 佛山市| 敦煌市| 宁陵县| 宕昌县| 万安县| 祥云县| 海林市| 兴安县| 凤翔县| 融水| 北宁市| 田林县| 宾川县| 古浪县| 金塔县| 通榆县| 宁阳县| 新乡市| 屯留县| 南投县| 内江市| 丽水市| 娱乐| 珠海市| 泗水县| 客服| 雷波县| 杭州市| 小金县| 建昌县| 社旗县| 吴旗县| 青冈县| 宜宾县| 和政县| 北辰区| 勃利县| 土默特右旗|