創(chuàng)建和啟動(dòng)Java線程
Java線程也是一個(gè)對象创葡,與任何其他Java對象一樣呕寝。線程是類 java.lang.Thread
的 實(shí)例矿酵,或此類的子類的實(shí)例改备。除了作為對象之外控漠,java線程還可以執(zhí)行代碼。在這個(gè)Java線程教程中悬钳,我將解釋如何創(chuàng)建和啟動(dòng)線程盐捷。
Java線程視頻教程
這里是這個(gè)Java線程教程的視頻版本:
https://www.youtube.com/watch?v=9y7l6QHpoQI
創(chuàng)建和啟動(dòng)線程
下面展示如何用Java創(chuàng)建一個(gè)線程:
Thread thread = new Thread();
要啟動(dòng)Java線程,你要調(diào)用其start()方法默勾,如下所示:
thread.start();
此示例并未指定線程要執(zhí)行的代碼碉渡,線程啟動(dòng)后會(huì)立即運(yùn)行結(jié)束,然后停止母剥。
有兩種方法可以指定線程需要執(zhí)行的代碼滞诺。第一種是創(chuàng)建Thread的子類并覆蓋該run()方法形导。第二種方法是傳遞一個(gè)實(shí)現(xiàn)了Runnable接口 (java.lang.Runnable
)的實(shí)例到Thread的構(gòu)造函數(shù)中。兩個(gè)方法都在下面介紹习霹。
Thread 子類
指定線程要運(yùn)行的代碼的第一種方法是創(chuàng)建Thread的子類并覆蓋run()方法朵耕。run()方法是調(diào)用start()方法后線程執(zhí)行的方法。以下是創(chuàng)建Java Thread子類的示例:
public class MyThread extends Thread {
public void run(){
System.out.println("MyThread running");
}
}
要?jiǎng)?chuàng)建并啟動(dòng)上述線程淋叶,您可以這樣做:
MyThread myThread = new MyThread();
myTread.start();
一旦線程啟動(dòng) 阎曹,start()方法調(diào)用將立即返回,它不會(huì)等到run()方法執(zhí)行完成煞檩。run()方法在執(zhí)行時(shí)就好像是由另一個(gè)不同的CPU在執(zhí)行一樣处嫌。當(dāng)run()方法執(zhí)行時(shí),它將打印出文本“MyThread running”斟湃。
您還可以創(chuàng)建一個(gè)這樣的匿名子類Thread:
Thread thread = new Thread() {
public void run(){
System.out.println("Thread Running");
}
}
thread.start();
一旦run()方法被該新線程執(zhí)行锰霜,此示例將打印出文本“Thread running” 。
Runnable 接口實(shí)現(xiàn)
指定線程要運(yùn)行的代碼的第二種方法是創(chuàng)建實(shí)現(xiàn)java.lang.Runnable
接口的類桐早。實(shí)現(xiàn)Runnable
接口的Java對象可以由Java Thread
來執(zhí)行癣缅。如何完成這一點(diǎn)將在本教程稍后介紹。
Runnable
接口是Java平臺(tái)附帶的標(biāo)準(zhǔn)Java接口哄酝。Runnable
接口僅具有一個(gè)方法run()
友存,這差不多是Runnable
接口的樣子:
public interface Runnable() {
public void run();
}
無論線程在執(zhí)行時(shí)要做什么,都必須包含在run()方法的實(shí)現(xiàn)中陶衅。有三種方法可以實(shí)現(xiàn)Runnable接口:
- 創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable接口的Java類屡立。
- 創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable接口的匿名類。
- 創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable接口的Java Lambda 搀军。
下面的部分將介紹這三個(gè)選項(xiàng)膨俐。
Java 類實(shí)現(xiàn)Runnable
實(shí)現(xiàn)Java Runnable接口的第一種方法是創(chuàng)建自己的Java類來實(shí)現(xiàn)Runnable接口。以下是實(shí)現(xiàn)Runnable接口的自定義Java類的示例:
public class MyRunnable implements Runnable {
public void run(){
System.out.println("MyRunnable running");
}
}
這個(gè)Runnable的實(shí)現(xiàn)內(nèi)容是打印 MyRunnable running
文本罩句。打印該文本后焚刺,run()方法退出,運(yùn)行run()方法的線程將停止门烂。
Runnable 的匿名實(shí)現(xiàn)
您還可以創(chuàng)建一個(gè)匿名實(shí)現(xiàn)Runnable的類乳愉。以下是實(shí)現(xiàn)Runnable接口的匿名Java類的示例:
Runnable myRunnable = new Runnable() {
public void run(){
System.out.println("Runnable running");
}
}
除了是一個(gè)匿名類之外,此示例與使用自定義類實(shí)現(xiàn)Runnable
接口的示例非常相似屯远。
Runnable 的Java lambda實(shí)現(xiàn)
實(shí)現(xiàn)Runnable
接口的第三種方法是創(chuàng)建一個(gè) Java Lambda 來實(shí)現(xiàn)Runnable
蔓姚。這是可以做到的,因?yàn)?code>Runnable接口只有一個(gè)未實(shí)現(xiàn)的方法慨丐,因此實(shí)際上(盡管可能是無意的)是一個(gè) 功能性Java接口坡脐。
以下是實(shí)現(xiàn)Runnable
接口的Java lambda表達(dá)式的示例:
Runnable runnable = () -> { System.out.println("Lambda Runnable running"); };
使用Runnable啟動(dòng)線程
要讓一個(gè)線程執(zhí)行run()方法,需要將實(shí)現(xiàn)Runnable接口的類房揭,匿名類或lambda表達(dá)式的實(shí)例傳遞給Thread
類的構(gòu)造函數(shù)中:
Runnable runnable = new MyRunnable(); // or an anonymous class, or lambda...
Thread thread = new Thread(runnable);
thread.start();
線程啟動(dòng)時(shí)备闲,它將調(diào)用 MyRunnable
實(shí)例的 run()
方法晌端,而不是執(zhí)行它自己的 run()
方法。上面的示例將打印出文本“MyRunnable running”浅役。
以下是Thread類的源碼:
@Override
public void run() {
if (target != null) {
target.run();
}
}
/* What will be run. */
private Runnable target;
從中可以看出run()方法的默認(rèn)實(shí)現(xiàn)就是調(diào)用Runnable接口的run()方法斩松,如果創(chuàng)建線程時(shí)傳遞給它一個(gè)Runnable對象伶唯。這里的成員變量target
為構(gòu)造函數(shù)中傳入的Runnable對象觉既,具體的初始化過程可以查看Thread源碼。
子類還是Runnable乳幸?
沒有規(guī)定兩種方法中的哪種方法最好瞪讼,兩種方法都能工作。就個(gè)人而言粹断,我更喜歡實(shí)現(xiàn) Runnable
符欠,將實(shí)現(xiàn)的實(shí)例交給 Thread
實(shí)例。當(dāng) Runnable
提交給線程池執(zhí)行時(shí)瓶埋,就可以很容易將 Runnable
實(shí)例排隊(duì)希柿,直到線程池的工作線程空閑然后執(zhí)行(關(guān)于線程池的機(jī)制在后面章節(jié)介紹)。而使用 Thread
子類完成這種工作就會(huì)有難度养筒。
有時(shí)你需要一起實(shí)現(xiàn) Runnable
和 Thread
子類曾撤。例如,創(chuàng)建一個(gè)可以執(zhí)行多個(gè) Runnable
的 Thread
子類 晕粪,實(shí)現(xiàn)線程池時(shí)通常就是這種情況挤悉。
常見陷阱:調(diào)用run() 而不是start()
在創(chuàng)建和啟動(dòng)線程時(shí),常見的錯(cuò)誤是調(diào)用Thread類的run()方法而不是start()巫湘,如下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run(); //should be start();
起初装悲,你可能沒有發(fā)現(xiàn)任何問題因?yàn)?Runnable
的 run()
像你期望的一樣執(zhí)行。但是尚氛,它不是由剛剛創(chuàng)建的新線程執(zhí)行的诀诊,相反,該 run()
方法由創(chuàng)建該線程的線程執(zhí)行阅嘶。換句話說畏梆,就是執(zhí)行上面兩行代碼的線程,這點(diǎn)可以從前面的Thread源碼中看出奈懒。要使新創(chuàng)建的線程調(diào)用 MyRunnable
實(shí)例的 run()
方法奠涌,必須調(diào)用 newThread.start()
方法。因?yàn)榫€程的狀態(tài)不止有運(yùn)行態(tài)磷杏,還會(huì)有等待溜畅,睡眠等狀態(tài)以及線程的上下文切換,直接調(diào)用run()方法只會(huì)執(zhí)行其中的代碼极祸,不會(huì)正常的創(chuàng)建一個(gè)線程慈格,也不會(huì)是真正的多線程執(zhí)行怠晴,關(guān)于線程的基本理論會(huì)在后面說明。
線程名稱
創(chuàng)建Java線程時(shí)浴捆,可以為其命名蒜田。該名稱可以幫助您區(qū)分不同的線程。例如选泻,如果多個(gè)線程寫入 System.out
冲粤,可以很方便地查看哪個(gè)線程寫入文本。這是一個(gè)例子:
Thread thread = new Thread("New Thread") {
public void run(){
System.out.println("run by: " + getName());
}
};
thread.start();
System.out.println(thread.getName());
注意字符串"New Thread"作為參數(shù)傳遞給 Thread
構(gòu)造函數(shù)页眯。該字符串是線程的名稱梯捕。該名稱可以通過 Thread
的 getName()
方法獲得。當(dāng)你使用Runnable
實(shí)現(xiàn)時(shí)你也可以傳遞名稱 :
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");
thread.start();
System.out.println(thread.getName());
但請注意窝撵,由于 MyRunnable
類不是 Thread
子類傀顾,因此它無權(quán)訪問執(zhí)行線程的 getName()
方法。
以下代碼是錯(cuò)誤的:
MyRunnable runnable = new MyRunnable();
runnable.getName(); //error
啟動(dòng)一個(gè)線程前碌奉,最好為這個(gè)線程設(shè)置名稱短曾,因?yàn)檫@樣在使用jstack分析程序或者進(jìn)行問題排查時(shí),就會(huì)給開發(fā)人員一點(diǎn)提示赐劣,自定義的線程最好的起個(gè)名字嫉拐。
Thread.currentThread()
Thread.currentThread()
方法返回執(zhí)行 currentThread()
方法的 Thread
實(shí)例的引用。這樣隆豹,你就可以在run()方法中訪問執(zhí)行自己的Java Thread
對象椭岩。以下是如何使用 Thread.currentThread()
的示例:
Thread thread = Thread.currentThread();
一旦有了 Thread
對象的引用,就可以在其上調(diào)用方法璃赡。例如判哥,您可以獲取正在執(zhí)行當(dāng)前代碼的線程的名稱,如下所示:
String threadName = Thread.currentThread().getName();
Java線程示例
這是一個(gè)小例子碉考。首先塌计,它打印出執(zhí)行 main()
方法的線程的名稱。該線程由JVM分配侯谁。然后它啟動(dòng)10個(gè)線程并給它們一個(gè)數(shù)字("" + i
)作為名稱锌仅。然后每個(gè)線程打印出其自己名稱,然后停止執(zhí)行墙贱。
public class ThreadExample {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
for(int i=0; i<10; i++) {
new Thread("" + i) {
public void run() {
System.out.println("Thread: " + getName() + " running");
}
}.start();
}
}
}
請注意热芹,即使線程按順序啟動(dòng)(1,2,3等),它們也可能不會(huì)按啟動(dòng)的順序執(zhí)行惨撇,這意味著線程1可能不是第一個(gè)將其名稱寫入 System.out
的線程伊脓。這是因?yàn)榫€程原則上并行執(zhí)行而不是順序執(zhí)行,由JVM和操作系統(tǒng)確定執(zhí)行線程的順序魁衙,順序不必與它們的啟動(dòng)順序相同??报腔。
暫停一個(gè) Thread
線程可以通過調(diào)用靜態(tài)方法 Thread.sleep()
來暫停自身株搔。 sleep()
方法需要毫秒數(shù)作為參數(shù)。sleep()
方法將在線程恢復(fù)執(zhí)行之前讓線程休眠指定的毫秒數(shù)纯蛾。sleep()
方法不是100%精確纤房,但它已經(jīng)足夠使用了。以下是通過調(diào)用 Thread.sleep()
方法暫停Java線程3秒(3,000毫秒)的示例:
try {
Thread.sleep(3L * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
執(zhí)行上述Java代碼的線程將休眠大約3秒(3,000毫秒)翻诉。
停止一個(gè) Thread
停止Java線程需要準(zhǔn)備一些線程實(shí)現(xiàn)代碼炮姨。Java Thread類
包含一個(gè) stop()
方法,但不推薦使用它米丘。原始 stop()
方法不會(huì)提供有關(guān)線程停止?fàn)顟B(tài)的任何保證剑令。這意味著糊啡,線程在執(zhí)行期間訪問的所有Java對象都將處于未知狀態(tài)拄查。如果應(yīng)用程序中的其他線程也訪問相同的對象,則應(yīng)用程序可能會(huì)意外地和不可預(yù)測地失敗棚蓄。
代替調(diào)用 stop()
方法堕扶,你需要實(shí)現(xiàn)你自己的線程代碼,以便可以停止它梭依。下面是一個(gè)實(shí)現(xiàn) Runnable
接口的類的示例稍算,其中包含一個(gè)額外的方法doStop(),該方法被調(diào)用來通知 Runnable
以停止役拴。Runnable
會(huì)檢查這個(gè)信號糊探,如果信號符合就停止。
public class MyRunnable implements Runnable {
private boolean doStop = false;
public synchronized void doStop() {
this.doStop = true;
}
private synchronized boolean keepRunning() {
return this.doStop == false;
}
@Override
public void run() {
while(keepRunning()) {
// keep doing what this thread should do.
System.out.println("Running");
try {
Thread.sleep(3L * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意 doStop()
和 keepRunning()
方法河闰。 doStop()
旨在被另一個(gè)線程調(diào)用執(zhí)行科平,而不是在 MyRunnable
的 run()
方法中使用。 keepRunning()
方法由執(zhí)行 MyRunnable
的 run()
方法的線程內(nèi)部調(diào)用姜性。只要 doStop()
尚未調(diào)用瞪慧,keepRunning()
方法將永遠(yuǎn)返回true - 意味著執(zhí)行該 run()
方法的線程將繼續(xù)運(yùn)行。
下面是一個(gè)執(zhí)行上述 MyRunnable
類實(shí)例的Java線程的示例部念,主線程在經(jīng)過一段延遲后停止該子線程:
public class MyRunnableMain {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
try {
Thread.sleep(10L * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
myRunnable.doStop();
}
}
此示例首先創(chuàng)建一個(gè) MyRunnable
實(shí)例弃酌,然后將該實(shí)例傳遞給一個(gè)線程并啟動(dòng)該線程。然后執(zhí)行main()方法的線程(主線程)休眠10秒儡炼,休眠結(jié)束后調(diào)用 MyRunnable
實(shí)例的doStop()方法妓湘。這將導(dǎo)致執(zhí)行 MyRunnable
的線程停止,因?yàn)?keepRunning()
將在 doStop()
調(diào)用之后返回 false
乌询。
請記住榜贴,如果你的 Runnable
實(shí)現(xiàn)不只需要 run()
方法(例如還需要一個(gè) stop()
或 pause()
方法),那么您就不能再使用Java lambda表達(dá)式創(chuàng)建實(shí)現(xiàn) Runnable
楣责。Java lambda只能實(shí)現(xiàn)單個(gè)方法竣灌。相反聂沙,您必須使用自定義類或繼承 Runnable
的自定義接口,該接口可以具有額外的方法初嘹,然后由匿名類實(shí)現(xiàn)及汉。
還有兩個(gè)類似
stop()
的方法是suspend()
和resume()
方法,第一個(gè)用于暫停線程屯烦,另一個(gè)用于恢復(fù)線程坷随,但是這些方法都和stop()
方法一樣被廢棄了。廢棄的原因主要有:以suspend()
方法為例驻龟,調(diào)用后温眉,線程不會(huì)釋放已經(jīng)占用的資源(比如鎖),而是占有資源進(jìn)行睡眠狀態(tài)翁狐,這樣容易引發(fā)死鎖問題类溢。同樣,stop()
方法在終結(jié)一個(gè)線程的時(shí)候也不會(huì)保證線程的資源正常釋放露懒,通常是沒有給予線程完成資源釋放工作的機(jī)會(huì)闯冷。
理解中斷
中斷可以理解為線程的一個(gè)標(biāo)識位屬性,他表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作懈词。中斷好比其他線程對該線程打了個(gè)招呼蛇耀,其他線程通過調(diào)用該線程的interrupt()
方法對其進(jìn)行中斷操作。
線程通過檢查自身是否被中斷來進(jìn)行響應(yīng)坎弯,線程可以通過方法isInterrupted()
來進(jìn)行判斷是否被中斷纺涤,也可以調(diào)用靜態(tài)方法Thread.interrupted()
對當(dāng)前線程的中斷標(biāo)識位進(jìn)行復(fù)位。如果該線程已經(jīng)處于終結(jié)狀態(tài)抠忘,即使該線程被中斷過撩炊,在調(diào)用該線程對象的isInterrupted()
方法時(shí)依然會(huì)返回false。
許多聲明拋出InterruptedException
的方法(例如Thread.sleep()
)在拋出異常前褐桌,Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)志位清除衰抑,然后拋出異常,此時(shí)isInterrupted()
方法將返回false荧嵌。
public class InterruptedTest {
/**
* result: SleepThread interrupted is false
* BusyThread interrupted is true
* SleepThread throw InterruptedException
* @throws Exception InterruptedException
*/
@Test
public void test() throws Exception {
Thread sleepThread = new Thread(()->{
while(true) {
SleepUtils.sleep(10);
}
}, "SleepThread");
sleepThread.setDaemon(true);
Thread busyThread = new Thread(()->{
while (true) { }
},"BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
//休眠呛踊,使兩個(gè)線程充分運(yùn)行
SleepUtils.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
//防止兩個(gè)線程立即退出
SleepUtils.sleep(2);
}
}
輸出結(jié)果如下:
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:340)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:403)
at util.SleepUtils.sleep(SleepUtils.java:8)
at p4.InterruptedTest.lambda$test$0(InterruptedTest.java:22)
at java.base/java.lang.Thread.run(Thread.java:844)
SleepThread interrupted is false
BusyThread interrupted is true
從結(jié)果可以看出,拋出InterruptedException
的線程SleepThread啦撮,中斷標(biāo)識位被清除了谭网,而一直忙碌的線程BusyThread,中斷標(biāo)識位沒有被清除赃春。
安全的停止線程
在前面已經(jīng)說過一個(gè)正確停止線程的方法愉择,不過在講解了中斷之后,也因此多了一種新的方式。現(xiàn)在對其進(jìn)行一點(diǎn)修改:
/**
* 中斷操作是一種簡便的線程交互方式锥涕,最適合用來取消或停止任務(wù)
* 除了中斷以外衷戈,還可以用一個(gè)boolean變量來控制是否需要停止任務(wù)并終止該線程
* 這兩種方法都可以在線程終止時(shí)釋放資源,而廢棄的stop()則不可以
*/
public class ShutDownTest {
@Test
public void test() throws Exception {
Runner one = new Runner();
Thread thread = new Thread(one, "CountThread");
thread.start();
SleepUtils.sleep(1);
thread.interrupt();
Runner two = new Runner();
thread = new Thread(two, "CountThread");
thread.start();
SleepUtils.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
++i;
}
System.out.println("count i : " + i);
}
void cancel() {
on = false;
}
}
}
輸出結(jié)果如下:
count i : 364312261
count i : 353178215
可以看出通過cancel()方法和中斷操作都可以將CountThread得以終止层坠。這兩種方式可以使線程在終止時(shí)有機(jī)會(huì)去清理資源殖妇,而不是武斷的直接將線程停止,因此這兩種方式顯得更加安全和優(yōu)雅破花。
競爭條件和臨界區(qū)
一個(gè)競爭條件 是一個(gè)可能在臨界區(qū)內(nèi)出現(xiàn)的特殊情況谦趣。臨界區(qū) 是被多個(gè)線程執(zhí)行的代碼區(qū)域,并且由于臨界區(qū)的同步執(zhí)行會(huì)導(dǎo)致線程執(zhí)行順序會(huì)出現(xiàn)差別座每。
當(dāng)多個(gè)線程執(zhí)行臨界區(qū)產(chǎn)生的結(jié)果因?yàn)榫€程執(zhí)行的順序而最終不同時(shí)前鹅,臨界區(qū)被稱為包含競爭條件。競爭條件一詞源于線程正在競爭通過臨界區(qū)的比喻峭梳,并且該競爭的結(jié)果影響執(zhí)行臨界區(qū)的結(jié)果舰绘。
這可能聽起來有點(diǎn)復(fù)雜,因此我將在以下章節(jié)中詳細(xì)介紹競爭條件和臨界區(qū)延赌。
臨界區(qū)
在同一個(gè)應(yīng)用程序中運(yùn)行多個(gè)線程不會(huì)導(dǎo)致問題除盏,但是當(dāng)多個(gè)線程訪問相同的資源時(shí)會(huì)出現(xiàn)問題叉橱。例如挫以,相同的內(nèi)存(變量,數(shù)組或?qū)ο螅┣宰#到y(tǒng)(數(shù)據(jù)庫掐松,Web服務(wù)等)或文件。
實(shí)際上粪小,只有一個(gè)或多個(gè)線程寫入這些資源時(shí)才會(huì)出現(xiàn)問題大磺。只要資源不改變,讓多個(gè)線程讀取相同的資源是安全的探膊。
這是一個(gè)臨界區(qū)Java代碼示例杠愧,如果同時(shí)由多個(gè)線程執(zhí)行,則可能會(huì)失敵驯凇:
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
想象一下流济,如果兩個(gè)線程A和B正在同一個(gè) Counter
類實(shí)例上執(zhí)行add方法,我們無法知道操作系統(tǒng)何時(shí)在兩個(gè)線程之間切換腌闯。 add()
方法中的代碼不是被Java虛擬機(jī)作為單個(gè)原子指令執(zhí)行的绳瘟,而是將其作為一組較小的指令執(zhí)行,類似于:
- 從內(nèi)存中讀取this.count到寄存器中姿骏。
- 添加值到寄存器糖声。
- 將寄存器寫入存儲(chǔ)器。
觀察以下線程A和B的混合執(zhí)行會(huì)發(fā)生什么:
this.count = 0;
A: Reads this.count into a register (0)
B: Reads this.count into a register (0)
B: Adds value 2 to register
B: Writes register value (2) back to memory. this.count now equals 2
A: Adds value 3 to register
A: Writes register value (3) back to memory. this.count now equals 3
兩個(gè)線程想要將值2和3添加到counter。因此蘸泻,在兩個(gè)線程完成執(zhí)行后琉苇,該值應(yīng)該為5。但是悦施,由于兩個(gè)線程的執(zhí)行是交錯(cuò)的翁潘,因此結(jié)果會(huì)不同。
在上面列出的執(zhí)行序列示例中歼争,兩個(gè)線程都從內(nèi)存中讀取值0拜马。然后,他們分別將他們自己的值2和3添加到其中沐绒,然后將結(jié)果寫回內(nèi)存俩莽。 this.count
中存儲(chǔ)的值將是最后一個(gè)線程寫入其中的值而不是5。在上面的例子中它是線程A乔遮,但如果執(zhí)行順序改變它也可以是線程B.
臨界區(qū)中的競爭條件
前面示例中 add()
方法中的代碼包含一個(gè)臨界區(qū)扮超。當(dāng)多個(gè)線程執(zhí)行此臨界區(qū)時(shí),會(huì)出現(xiàn)競爭條件蹋肮。
更正式地說出刷,遇到兩個(gè)線程競爭相同資源的情況,其中訪問資源的順序是重要的坯辩,稱為競爭條件馁龟。導(dǎo)致競爭條件的代碼部分稱為臨界區(qū)。
防止競爭條件
為了防止競爭條件發(fā)生漆魔,您必須確保臨界區(qū)作為原子指令執(zhí)行坷檩。這意味著一旦一個(gè)線程執(zhí)行它,在第一個(gè)線程離開臨界區(qū)之前改抡,沒有其他線程可以執(zhí)行它矢炼。
通過在臨界區(qū)使用適當(dāng)?shù)木€程同步機(jī)制可以避免競爭條件,可以使用同步的Java代碼塊來實(shí)現(xiàn)線程同步阿纤。線程間的同步也可以使用其他同步結(jié)構(gòu)來實(shí)現(xiàn)句灌,例如鎖或原子變量,如java.util.concurrent.atomic.AtomicInteger欠拾。
臨界區(qū)吞吐量
對于較小的臨界區(qū)胰锌,將整個(gè)臨界區(qū)包含在同步塊可以起作用。但是清蚀,對于較大的臨界區(qū)匕荸,將它分解為幾個(gè)較小的臨界區(qū)可能是有益的,這將允許多個(gè)線程同時(shí)執(zhí)行多個(gè)較小的臨界區(qū)枷邪,可以減少對共享資源的競爭榛搔,從而增加總臨界區(qū)的吞吐量诺凡。
這是一個(gè)非常簡單的Java代碼示例,用于表達(dá)我的意思:
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
public void add(int val1, int val2){
synchronized(this){
this.sum1 += val1;
this.sum2 += val2;
}
}
}
注意該add()
方法將值添加到兩個(gè)不同的sum成員變量践惑。為了防止競爭條件腹泌,求和在Java同步塊內(nèi)執(zhí)行。使用此實(shí)現(xiàn)尔觉,只有一個(gè)線程可以執(zhí)行求和凉袱。
但是,由于兩個(gè)sum變量彼此獨(dú)立侦铜,因此可以將它們的求和分成兩個(gè)獨(dú)立的同步塊专甩,如下所示:
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1);
private Integer sum2Lock = new Integer(2);
public void add(int val1, int val2){
synchronized(this.sum1Lock){
this.sum1 += val1;
}
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
現(xiàn)在兩個(gè)線程可以同時(shí)執(zhí)行該add()
方法,第一個(gè)同步塊內(nèi)有一個(gè)線程钉稍,第二個(gè)同步塊內(nèi)有另一個(gè)線程涤躲。兩個(gè)同步塊在不同對象上同步,因此兩個(gè)不同的線程可以獨(dú)立的分別執(zhí)行這兩個(gè)塊贡未。這樣線程執(zhí)行該add()
方法就可以彼此等待更少的時(shí)間种樱。
當(dāng)然,這個(gè)例子非常簡單俊卤。在現(xiàn)實(shí)生活中的共享資源中嫩挤,臨界區(qū)的分解可能要復(fù)雜得多,并且需要對執(zhí)行順序可能性進(jìn)行更多分析消恍。
線程安全和共享資源
可以被多個(gè)線程同時(shí)安全調(diào)用的代碼稱為線程安全岂昭。如果一段代碼是線程安全的,那么它不包含競爭條件哺哼,僅當(dāng)多個(gè)線程更新共享資源時(shí)才會(huì)出現(xiàn)競爭條件佩抹。因此,了解Java線程在執(zhí)行時(shí)需要共享的資源非常重要取董。
局部變量
局部變量存儲(chǔ)在每個(gè)線程自己的堆棧中,這意味著線程之間永遠(yuǎn)不會(huì)共享局部變量无宿,這也意味著所有原始類型局部變量(primitive variable茵汰,例如int,long等)都是線程安全的孽鸡。以下是線程安全局部原始類型變量的示例:
public void someMethod(){
long threadSafeInt = 0;
threadSafeInt++;
}
局部對象引用
局部對象引用和原始類型變量有點(diǎn)不同蹂午,引用自己本身不共享。但是彬碱,引用的對象不存儲(chǔ)在每個(gè)線程的本地堆棧中豆胸,所有對象都存儲(chǔ)在共享堆中。
如果一個(gè)方法創(chuàng)建的對象永遠(yuǎn)不會(huì)離開創(chuàng)建它的方法巷疼,那么它是線程安全的晚胡。事實(shí)上,您也可以將其傳遞給其他方法和對象,只要這些方法或?qū)ο蠖疾粫?huì)使此對象能夠被其他線程使用估盘。
以下是線程安全局部對象的示例:
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
此示例中LocalObject
的實(shí)例不從方法返回瓷患,也不會(huì)傳遞給可從someMethod()
方法外部訪問的任何其他對象。執(zhí)行someMethod()
方法的每個(gè)線程將創(chuàng)建自己的LocalObject
實(shí)例并將其分配給localObject
引用遣妥。因此擅编,這里LocalObject
的使用是線程安全的。
實(shí)際上箫踩,整個(gè)方法someMethod()
都是線程安全的爱态。即使LocalObject
實(shí)例作為參數(shù)傳遞給同一個(gè)類或其他類中的其他方法,它的使用也是線程安全的境钟。
當(dāng)然肢藐,唯一的例外是,如果一個(gè)方法使用LocalObject
作為調(diào)用參數(shù)吱韭,并且以允許其他線程訪問的方式存儲(chǔ)這個(gè)LocalObject
實(shí)例吆豹。
下面這個(gè)示例展示了上面描述的例外情況:
import org.junit.Test;
import static java.lang.Thread.sleep;
public class LocalObjectTest {
private LocalObject sharedLocalObject;
private int num;
@Test
public void test() throws Exception {
method1();
for(int i = 0; i < 4; ++i) {
new Thread(() -> sharedLocalObject.setText("" + num++)).start();
}
sleep(500);
System.out.println(sharedLocalObject.text);
}
/**
* 創(chuàng)建一個(gè)局部對象引用,并引用一個(gè)實(shí)例
*/
private void method1() {
LocalObject localObject = new LocalObject();
method2(localObject);
}
/**
* 使一個(gè)局部對象逃逸理盆,以此被其他線程訪問
* @param object LocalObject
*/
private void method2(LocalObject object) {
sharedLocalObject = object;
}
private static class LocalObject {
private String text;
void setText(String text) {
this.text = text;
}
}
}
sharedLocalObject
即是可從someMethod
方法外部訪問的對象痘煤,此對象可以被其他線程訪問(因?yàn)榇藢ο笫穷惖某蓡T變量,而線程和嵌套子類一樣猿规,可以在run()中訪問此對象)衷快。這個(gè)示例解釋了上面這句比較抽象的話:
此示例中
LocalObject
的實(shí)例不從方法返回,也不會(huì)傳遞給可從someMethod()
方法外部訪問的任何其他對象姨俩。
對象成員變量
對象成員變量(字段)與對象一起存儲(chǔ)在堆上蘸拔。因此,如果兩個(gè)線程在同一對象實(shí)例上調(diào)用方法环葵,并且此方法更新成員變量调窍,則該方法不是線程安全的。以下是非線程安全方法的示例:
public class NotThreadSafe{
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
如果兩個(gè)線程在同一個(gè)NotThreadSafe實(shí)例上同時(shí)調(diào)用add()
方法张遭,則會(huì)導(dǎo)致競爭條件邓萨。例如:
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
}
注意兩個(gè)MyRunnable
實(shí)例共享同一個(gè)NotThreadSafe
實(shí)例。因此菊卷,當(dāng)他們在NotThreadSafe
實(shí)例上調(diào)用add()
方法時(shí)缔恳,會(huì)導(dǎo)致競爭條件。
但是洁闰,如果兩個(gè)線程在不同的實(shí)例上同時(shí)調(diào)用add()
方法 歉甚,那么它不會(huì)導(dǎo)致競爭條件。以下是之前的示例扑眉,但略有修改:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
現(xiàn)在兩個(gè)線程各有自己的NotThreadSafe
實(shí)例纸泄,因此它們對add方法的調(diào)用不會(huì)彼此干擾赖钞,代碼不再具有競爭條件。因此刃滓,即使對象不是線程安全的仁烹,它仍然可以以不會(huì)導(dǎo)致競爭條件的方式使用。
線程控制逃逸規(guī)則
在嘗試確定您的代碼對某個(gè)資源的訪問是否是線程安全時(shí)咧虎,您可以使用線程控制逃逸規(guī)則:
If a resource is created, used and disposed within
the control of the same thread,
and never escapes the control of this thread,
the use of that resource is thread safe.
如果資源的創(chuàng)建卓缰,使用和回收在同一個(gè)線程控制下進(jìn)行,
并且永遠(yuǎn)不會(huì)從這個(gè)線程的控制下逃逸砰诵,那么這個(gè)資源的使用是線程安全的征唬。
資源可以是任何共享資源,如對象茁彭,數(shù)組总寒,文件,數(shù)據(jù)庫連接理肺,套接字等摄闸。在Java中,您并不需要顯式地回收對象妹萨,因此“回收”意味著丟失對象的引用(引用另一個(gè)對象)或?qū)⒁弥脼閚ull年枕。
即使對象的使用是線程安全的挣菲,但是如果該對象指向共享資源(如文件或數(shù)據(jù)庫)迂猴,則整個(gè)應(yīng)用程序可能不是線程安全的。例如嗤疯,如果線程1和線程2各自創(chuàng)建自己的數(shù)據(jù)庫連接树姨,連接1和連接2摩桶,則每個(gè)連接本身的使用是線程安全的。但是連接指向的數(shù)據(jù)庫的使用可能不是線程安全的帽揪。例如硝清,如果兩個(gè)線程都執(zhí)行如下代碼:
check if record X exists
if not, insert record X
檢查記錄X是否存在
如果沒有,插入記錄X.
如果兩個(gè)線程同時(shí)執(zhí)行此操作台丛,并且它們正在檢查的記錄X恰好是相同的記錄耍缴,則存在兩個(gè)線程最終都插入它的風(fēng)險(xiǎn)。這是一個(gè)示例:
Thread 1 checks if record X exists. Result = no
Thread 2 checks if record X exists. Result = no
Thread 1 inserts record X
Thread 2 inserts record X
線程1檢查記錄X是否存在挽霉。結(jié)果為否
線程2檢查記錄X是否存在。結(jié)果為否
線程1插入記錄X.
線程2插入記錄X.
對于在文件或其他共享資源上進(jìn)行操作的線程也可能發(fā)生這種情況变汪。因此侠坎,區(qū)分由線程控制的對象是資源還是僅僅引用這個(gè)資源(如數(shù)據(jù)庫連接所做的)是很重要的。
線程安全和不變性
僅當(dāng)多個(gè)線程正在訪問同一資源裙盾,并且一個(gè)或多個(gè)線程寫入資源時(shí)实胸,才會(huì)出現(xiàn)競爭條件他嫡。如果多個(gè)線程讀取相同的資源, 競爭條件不會(huì)發(fā)生庐完。
我們可以確保線程之間共享的對象永遠(yuǎn)不會(huì)被任何線程更新钢属,方法是使共享對象不可變,從而保證線程安全门躯。這是一個(gè)例子:
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
注意ImmutableValue
實(shí)例的值是在構(gòu)造函數(shù)中傳遞的淆党,另請注意沒有setter方法。一旦ImmutableValue
實(shí)例被創(chuàng)建讶凉,你將不能改變它的值染乌,它是不可變的。但是懂讯,您可以使用getValue()
方法讀取它荷憋。
如果需要對ImmutableValue
實(shí)例執(zhí)行操作,可以通過返回帶有該操作產(chǎn)生的值的新實(shí)例來執(zhí)行此操作褐望。以下是添加操作的示例:
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public ImmutableValue add(int valueToAdd){
return new ImmutableValue(this.value + valueToAdd);
}
}
注意add()
方法返回了一個(gè)帶有add操作結(jié)果的新ImmutableValue
實(shí)例勒庄,而不是將值添加到自身。
引用不是線程安全的!
要記住瘫里,即使對象是不可變的并且因此線程安全实蔽,該對象的引用也可能不是線程安全的〖跣看看這個(gè)例子:
public class Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
}
Calculator
類持有一個(gè)對ImmutableValue
實(shí)例的引用盐须。注意可以通過setValue()
和add()
方法更改該引用。因此漆腌,即使Calculator
類在內(nèi)部使用不可變對象贼邓,它本身也不是不可變的,因此不是線程安全的闷尿,這和前面的累加器實(shí)現(xiàn)一樣塑径,add()和setValue()方法都不是原子操作,而是幾個(gè)操作組成填具。換句話說:ImmutableValue
是線程安全的统舀,但使用它不是。在嘗試通過不變性實(shí)現(xiàn)線程安全時(shí)劳景,請記住這一點(diǎn)誉简。
為了使Calculator
類線程安全,你可以使用synchronized關(guān)鍵詞聲明getValue()
盟广, setValue()
和add()
方法 闷串,那將可以做到。
原網(wǎng)頁: