(四) Java多線程內(nèi)存模型

Java多線程目錄

一 背景介紹

1 并發(fā)編程有兩個關(guān)鍵問題需要處理

1.1 通信

通信是指線程之間的信息交換,在命令式編程中有兩種方式。

  • 共享內(nèi)存
    線程之間共享程序的公共狀態(tài),通過讀/寫內(nèi)存中的公共狀態(tài)進行隱式同信。
  • 消息傳遞
    線程之間沒有公共狀態(tài),必須通過消息來進行通信

1.2 同步

同步是指用于控制不同的線程并發(fā)執(zhí)行的順序的一種方式。共享內(nèi)存并發(fā)同步是顯示指定的例如synchronized,程序員必須指定某個方法或者代碼線程之間互斥執(zhí)行。消息傳遞由于消息接受必定發(fā)生在消息發(fā)送之后,所以消息傳遞的同步是隱式的不需要程序員指定。

1.3 總結(jié)

Java的并發(fā)編程采用的是內(nèi)存共享模型,通信方式都是隱式的(不是真正的相互通信),但通信過程是透明的,可由程序員自己控制,也就是控制同步的方式。

2 Java線程內(nèi)存模型的抽象結(jié)構(gòu)

Java線程內(nèi)存模型

了解JVM內(nèi)存結(jié)構(gòu)的可以知道,主內(nèi)存也就是共享內(nèi)存,JVM中線程間共享的數(shù)據(jù)內(nèi)存一般都存放在堆中,這里包含類的對象,靜態(tài)域,數(shù)組等。
從上圖中可以看出Java內(nèi)存模型底層由JVM控制,JVM決定了共享的變量何時對另一個線程可見。每個線程都有一個主內(nèi)存的副本,我們在線程A中修改的數(shù)據(jù)就是修改這個本地內(nèi)存數(shù)據(jù),JVM會在一個合適的時機將這個改變寫入到主內(nèi)存,再讀入到另一個線程B的線程本地內(nèi)存中,這樣就達到了線程見通信的目的。
注意:線程的本地內(nèi)存是個抽象的概念 ,它包含了很多東西, 你可以理解為一個多線程讀寫主內(nèi)存的一個過程。
舉例

public class ThreadUser {
    private int id;
}


public class ThreadOne extends Thread {
    private ThreadUser user;

    public ThreadOne(ThreadUser user) {
        this.user = user;
    }

    @Override
    public void run() {
        super.run();
        user.setId(user.getId()+1);
    }

    public static void main(String[] args) {
        ThreadUser user = new ThreadUser();  //user共享的數(shù)據(jù)
        user.setId(1);
        ThreadOne one = new ThreadOne(user); //新的線程
        one.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(user.getId()); //取出user的新值
    }
}

從上面例子中我們可以分析,里面有兩個線程,主線程main, 子線程ThreadOne,主要步驟:

  1. main線程創(chuàng)建ThreadUser類對象,這個類對象是在堆內(nèi)存中的,是線程共享的。
  2. main線程設(shè)置ThreadUser對象ID為1, 設(shè)置之后jvm會在一個合適的時機將這個改變輸入到主內(nèi)存中去。
  3. 創(chuàng)建ThreadOne對象,進行多線程操作,start函數(shù)執(zhí)行,ThreadUser類的共享對象就會復(fù)制一份進入ThreadOne線程,ThreadOne線程對復(fù)制的ThreadUser對象ID進行了改變?yōu)?,后JVM回將這個改變刷入到主內(nèi)存中。
  4. main線程讀取,就是從主線程讀取到本地內(nèi)存,再取出ThreadUser對象的ID,這時main線程就可以得到ThreadOne線程中設(shè)置的2.

3 線程安全問題

從上文中可以看到Java命令操作數(shù)據(jù)都是在內(nèi)存上操作,上面的例子比較簡單所以不會出現(xiàn)線程安全問題,當多個線程同時操作一個ThreadUser對象的時候,就會發(fā)生線程安全問題。

3.1 線程安全問題的產(chǎn)生

線程安全問題就是在多個線程同時操作一個變量的時候,線程對主內(nèi)存的讀寫并沒有按照我們的預(yù)期執(zhí)行。例如兩個線程同時操作ThreadUser的ID,如果兩個線程同時讀取的ThreadUser對象中的ID的值為1,
但在兩個線程中同時執(zhí)行user.setId(user.getId()+1) 操作時user的id 最后都會為2,這時連個兩個線程都會將改變的本地內(nèi)存變量刷入到主內(nèi)存,則主內(nèi)存user對象的id則為2,但是我們進行了兩次相加,本該為3的,這就是線程的安全。


多線程數(shù)據(jù)安全

如圖:多線程的執(zhí)行順序一般我們無法控制,我們想的是連個線程一個加1一個加2這樣我們就能得到4,但結(jié)果卻得到了3或者2還有4。這里線程A和線程B執(zhí)行的順序有三種可能

  1. 正常執(zhí)行,線程A執(zhí)行完刷入內(nèi)存后線程B執(zhí)行沒有錯誤。
  2. 結(jié)果為3,線程A和線程B同時讀取了a=1,都進行了相加操作后,線程B結(jié)果刷入主內(nèi)存,后線程A結(jié)果又刷入主內(nèi)存,線程A對線程B結(jié)果進行了覆蓋。
  3. 同結(jié)果3,是線程B對線程A的結(jié)果進行了覆蓋。

4 順序一致性

如上文所示,數(shù)據(jù)為正確同步,就會存在數(shù)據(jù)競爭,執(zhí)行的順序與我們構(gòu)想的順序不一致,這就會造成各種各樣的問題。我們需要的多線程執(zhí)行是應(yīng)該具有順序一致性的,這樣我們的程序才會正確執(zhí)行。Java使用了各種各樣的同步方式來實現(xiàn)這個順序一致性,如volatile synchronized final關(guān)鍵字等。

4.1 順序一致性的理解

順序一致性內(nèi)存模型

如圖順序一致性內(nèi)存模型中有一個全局內(nèi)存,Java中就是我們的共享變量,內(nèi)存通過每次選擇一個線程來進行讀寫順序操作,來保證共享變量的正確性。
Java代碼例子,使用synchronized來實現(xiàn)。

public class ThreadOne implements Runnable{

    private Object lock = new Object();
    
    public void run() {
        synchronized (lock) {
            //TODO
        }
    }
}

如上述例子,我們對lock對象實例加了鎖,當多線程同時訪問一個ThreadOne對象的時候,每次只能有一個線程獲得這個鎖進行執(zhí)行,后續(xù)synchronized關(guān)鍵字會詳細介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容