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)

了解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,主要步驟:
- main線程創(chuàng)建ThreadUser類對象,這個類對象是在堆內(nèi)存中的,是線程共享的。
- main線程設(shè)置ThreadUser對象ID為1, 設(shè)置之后jvm會在一個合適的時機將這個改變輸入到主內(nèi)存中去。
- 創(chuàng)建ThreadOne對象,進行多線程操作,start函數(shù)執(zhí)行,ThreadUser類的共享對象就會復(fù)制一份進入ThreadOne線程,ThreadOne線程對復(fù)制的ThreadUser對象ID進行了改變?yōu)?,后JVM回將這個改變刷入到主內(nèi)存中。
- 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的,這就是線程的安全。

如圖:多線程的執(zhí)行順序一般我們無法控制,我們想的是連個線程一個加1一個加2這樣我們就能得到4,但結(jié)果卻得到了3或者2還有4。這里線程A和線程B執(zhí)行的順序有三種可能
- 正常執(zhí)行,線程A執(zhí)行完刷入內(nèi)存后線程B執(zhí)行沒有錯誤。
- 結(jié)果為3,線程A和線程B同時讀取了a=1,都進行了相加操作后,線程B結(jié)果刷入主內(nèi)存,后線程A結(jié)果又刷入主內(nèi)存,線程A對線程B結(jié)果進行了覆蓋。
- 同結(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)存,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)鍵字會詳細介紹。