# **線程與多線程的概念**
關于線程與多線程的較詳細的理解可以參考:[線程的解釋](http://baike.baidu.com/link?url=EUXhNs__U5gPEKvPq82eddOisibw8RaLiiNXy7VXxeRFE5Xq1MVgXdMunLvjJ4hsgFHyWpC0nlDo1B9KAm76Da) 和[多線程的解釋](http://baike.baidu.com/link?url=NWpAVlbQwePoTOsMzJsWOfk4Fz_-67HKGxTekmW2S1vSv7pGsqx70kWXfzJaPIlA)。
而我們要做的是,對其進行“精煉"。我們每天都在和電腦、手機打交道,每天都在使用各種各樣的應用軟件。
打開上電腦的任務管理器,就可以看到有一項名為"進程"的欄目,點擊到里面可能就會發現一系列熟悉的名稱:QQ,360等等。
所以首先知道了,QQ、360之類的應用軟件在計算機上被稱為一個進程。
而一個應用程序都會有自己的功能,用以執行這些進程當中的個別功能的程序執行流就是所謂的線程。
所以,線程有時候也被稱為輕量級進程,是程序執行流當中的最小單元。
線程的劃分尺度小于進程,其不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,所以能極大地提高了程序的運行效率。
所以簡而言之的概括的話,就是:一個程序至少有一個進程,一個進程至少有一個線程。
以360殺毒來說,里面的一項功能任務“電腦體檢”就是該應用程序進程中的一個線程任務。
而除此任務之外,我們還可以同時進行多項操作。例如:“木馬查殺”、“電腦清理”等。
那么,以上同時進行的多項任務就是所謂的存活在360應用程序進程中的多線程并發。
# 多線程的利與弊
多線程的有利之處,顯而易見。在傳統的程序設計語言中,同一時刻只能執行單任務操作,效率非常低。
假設在某個任務執行的過程中發生堵塞,那么下一個任務就只能一直等待,直至該任務執行完成后,才能接著執行。
而得益于多線程能夠實現的并發操作,即使執行過程中某個線程因某種原因發生阻塞,也不會影響到其它線程的執行。
也就是說,多線程并發技術帶來的最大好處就是:很大程度上提高了程序的運行效率。
似乎百里而無一害的多線程并發技術,還有弊端嗎?從某種程度上來說,也是存在的:會導致任務執行效率的降低。
之所以這樣講,是因為所謂的“并發”并不是真正意義上的并發,而是CPU在多個線程之間做著快速切換的操作。
但CPU的運算速度肯定是遠遠高于人類的思維速度,所以就帶來了一種“并發”的錯覺。
那就不難想象了:假設某一進程中,線程A與線程B并發執行,CPU要做的工作就是:
不斷快速且隨機的在兩個線程之間做著切換,分別處理對應線程上的線程任務,直到兩個線程上的任務都被處理完成。
那么,也就可以考慮這樣的情況:CPU執行完原本線程A的線程任務只需要5秒;但如今因為另一個線程B的并發加入。
CPU則不得不分出一部分時間切換到線程B上進行運算處理。于是可能CPU完成該線程任務A的時間反而延長到了7秒。
所以所謂的效率降低,就是指針對于某單個任務的執行效率而言的。
也就是說,如果在多線程并發操作時,如果有某個線程的任務你認為優先級很高。那么則可以:
通過設置線程優先級或者通過代碼控制等手段,來保證該線程享有足夠的“特權”。
注:Java中設置線程優先級,實際上也只是設置的優先級越大,該線程被CPU隨機訪問到的概率會相對高一些。
這個過程可以替換成一些實際生活中的情形來進行思考。快過年了,以家庭團聚為例。
假設你除了準備炒一桌子美味的菜肴之外,過年自然還要有一頓熱騰騰的餃子。那么:
傳統單任務的操作過程可以被理解為:先把準備的菜肴都做好;菜都端上桌后便開始煮餃子。
這樣做的壞處就是:如果在炒菜的中途發生一些意外情況,那么隨著炒菜動作的暫停。煮餃子的動作也將被無限期延后。
而對應于多線程并發的操作就是:一邊炒菜,一邊煮餃子。這時你就是CPU,你要做的動作可能是這樣的:
炒菜的中途你能會抽空去看看鍋里的餃子煮好沒有;發現沒有煮好,又回來繼續炒菜。炒好一道菜后,再去看看餃子能出鍋了沒。
由此你發現,你做的工作與CPU處理多線程并發的工作是一樣的:不斷的在“煮餃子”與“炒菜”兩個任務之間做著切換。
# 線程的周期及狀態
Java中線程的整個生命周期基本可以劃分為如下4種狀態:
- **new**- 創建狀態:顧明思議,Java通過new創建了一個線程對象過后,該線程就處于該狀態。
- **runnable**- 可執行狀態:也就是指在線程對象調用start()方法后進入的狀態。但需要注意的是該狀態是“可執行狀態”而不是“執行狀態”。也就是說,當一個線程對象調用start方法后,只是意味著它獲取到了CPU的執行資格,并不代表馬上就會被運行(CPU此時當然可能恰好切換在其它線程上做處理),只有具備了CPU當前執行權的線程才會被執行。
- **non Runnable**- 不可執行/阻塞狀態:也就是通過一些方法的控制,使該線程暫時釋放掉了CPU的執行資格的狀態。但此時該線程仍然是存在于內存中的。
- **done**-退出狀態:簡單的說也就是當線程進入到退出狀態,就意味著它消亡了,不存在了。Java里通過stop方法可以強制線程退出,但該方法因為可能引起死鎖,所以是不建議使用的。另外一種進入該狀態的方式,是線程的自然消亡,也就當一個線程的任務被執行完畢之后,就會自然的進入到退出狀態。
以下是Java中一些用于改變線程狀態的方法列表:

# Java中創建線程的方式
Java里面創建的線程的方式主要分為:
- 繼承Thread類,并覆寫run方法。
~~~
public class Demo extends Thread{
@Override
public void run() {
//...
}
}
~~~
- 實現Runnable接口,并定義run方法:
~~~
public class Demo implements Runnable{
@Override
public void run() {
//...
}
}
~~~
- 還有一種情況,如果你認為沒有將線程單獨封裝出來的時候,可以通過匿名內部類來實現。
開發中通常選擇通過實現Runnbale接口的方式創建線程,好處在于:
1.Java中不支持多繼承,所以使用Runnable接口可以避免此問題。
2.實現Runnable接口的創建方式,等于是將線程要執行的任務單獨分離了出來,更符合OO要求的封裝性。
# 多線程的安全隱患
春運將至了,還是先通過一個老話題來看一個多線程并發的例子,來看看多線程可能存在的安全隱患。
~~~
package com.tsr.j2seoverstudy.thread;
public class TicketDemo {
public static void main(String[] args) {
Runnable sale = new TicketOffice();
Thread t1 = new Thread(sale, "1號售票窗口");
Thread t2 = new Thread(sale, "2號售票窗口");
t1.start();
t2.start();
}
}
class TicketOffice implements Runnable {
// 某車次的車票存量
private int ticket_num = 10;
@Override
public void run() {
while (true) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num-- + "號票";
System.out.println(output);
} else {
break;
}
}
}
}
/*
可能出現如下的輸出結果:
2號售票窗口售出了10號票
1號售票窗口售出了9號票
1號售票窗口售出了8號票
2號售票窗口售出了7號票
1號售票窗口售出了6號票
2號售票窗口售出了5號票
1號售票窗口售出了4號票
2號售票窗口售出了3號票
1號售票窗口售出了2號票
2號售票窗口售出了1號票
1號售票窗口售出了0號票
*/
~~~
按我們的理想的想法是:兩個售票處共同完成某車次列車的10張車票:座位號為1號到10號的車票的售票工作。
而根據程序的輸出結果,我們發現的安全隱患是:有座位號為0號的車票被售出了,買到這張車票的顧客該找誰說理去呢?
我們來分析一下為什么會出現這樣的錯誤情況,其形成的原因可能是這樣的:
當線程1執行完“1號售票窗口售出了2號票”之后,根據while循環的規則,再一次開始售票工作。
首先判斷while為true,進入到while循環體;接著判斷if語句,此時余票數為1張(也就是只剩下座位號為1的車票了)。
1大于0,滿足判斷條件,進入到if語句塊當中。此時執行到"Thread.sleep(10)"語句。
OK,當前線程進入到堵塞狀態,暫時失去了Cpu的執行資格。于是Cpu重新切換,開始執行線程2。
于是線程2開始執行線程任務,又是老樣子:while判斷 - if判斷,由于上次線程1判斷后還沒執行售票工作,就被阻塞了。
所以這次if判斷仍然為"1>0",滿足判斷條件,繼續執行,又執行到線程休眠語句,于是線程2也進入阻塞狀態。
此時兩個線程暫時都不具備執行資格,但我們指定線程休眠的時間為10毫秒,于是10毫秒后,可能兩個線程都蘇醒了,恢復了Cpu的執行資格。
面對兩個都處于可執行狀態的線程,Cpu又只好隨機選擇一個先執行了。于是Cpu選擇了線程2,線程2恢復執行。
線程2開始做自己上次沒做完的事,于是執行表達式和輸出語句,于是得到輸出信息"2號售票窗口售出了1號票"。
線程2繼續執行while判斷,沒問題。再執行if判斷"0>0",不滿足判斷條件,于是執行到了break語句。
線程2到此退出循環,完成了所有線程任務,于是自然消亡進入done狀態。
于是現在Cpu的執行權自然就屬于線程1了,線程1也如同線程2一樣,從美夢中醒來,開始上次沒做完的事。
問題就在這里出現了,雖然這個時候,堆內存中存放的對象成員變量“ticket_num”的值實際上已經是0了。
但是!因為上一次線程1已經經過了if判斷進入到了if語句塊之內。所以它將直接開始執行表達式,并輸出。
就形成了我們看到的錯誤信息:“1號售票窗口售出了0號票”。并且這個時候實際上余票數的值已經是“-1”了。
所以,實際上之所以我們在處理賣票的代碼之前加上讓線程休眠10毫秒的代碼,目的也就是為了模擬線程安全隱患的問題。
而根據這個例子我們能夠得到的信息就是:之所以多線程并發存在著安全隱患,正是CPU的實際處理方式是在不同線程之間做著隨機的快速切換。
這意味著它并不會保證當處理一個線程的任務時,一定會執行完該次線程的所有代碼才做切換。而是可能做到一半就切換了。
所以,我們可以歸納線程安全隱患之所以會出現的原因就是因為:
- 多個并發線程操作同一個共享數據。
- 操作該共享數據的代碼不止一行,存在多行。
# 解決線程安全隱患的方法 - 同步鎖
既然已經了解了線程安全隱患之所以產生,就是因為線程在操作共享數據的途中,其它線程被參與了進來。
那么我們想要解決這一類的安全隱患,自然就是保證在某個線程在執行線程任務的時候,不能讓其余線程來搗亂。
在樣的做法,在Java當中被稱為同步鎖,也就是說給封裝在同步當中的代碼加上一把鎖。
每次只能由一個線程能夠獲取到這把鎖,只有當前持有鎖的線程才能執行同步當中的代碼,其它線程將被拒之門外。
Java中對于同步的使用方式通常分為兩種,即:同步代碼塊和同步函數。關鍵字synchronized用以聲明同步。其格式分別為:
~~~
//同步代碼塊
synchronized (對象鎖) {
//同步代碼
}
//同步函數
synchronized void method(){
//同步代碼
}
~~~
通過同步我們就可以解決上面所說的“春節賣票”問題的安全隱患:
~~~
package com.tsr.j2seoverstudy.thread;
public class TicketDemo {
public static void main(String[] args) {
Runnable sale = new TicketOffice();
Thread t1 = new Thread(sale, "1號售票窗口");
Thread t2 = new Thread(sale, "2號售票窗口");
t1.start();
t2.start();
}
}
class TicketOffice implements Runnable {
// 某車次的車票存量
private int ticket_num = 10;
Object objLock = new Object();
@Override
public void run() {
while (true) {
synchronized (objLock) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num-- + "號票";
System.out.println(output);
} else {
break;
}
}
}
}
}
~~~
再次運行該代碼,就不會再出現之前的安全隱患。
這正是因為我們通過同步代碼塊,將希望每次只有有一個線程執行的代碼封裝了起來,為它們加上了一把同步鎖(對象)。
同步最需要注意的地方,就是要保證鎖的一致性。這是因為我們說過了:
同步的原理就是鎖,每次當有線程想要訪問同步當中的代碼的時候,只有獲取到該鎖才能執行。
所以如果鎖不能保證是同一把的話,自然也就實現不了所謂的同步了。
可以試著將定義在TicketOffice的成員變量objLock移動定義到run方法當中,就會發現線程安全問題又出現了。
這正是因為,將對象類型變量objLock定義為成員變量,它會隨著該類的對象存儲在堆內存當中,該變量在內存中獨此一份。
而移動到run方法內,則會存儲在棧內存當中,而每一個線程都會在棧內存中,單獨開辟一條方法棧。
這樣就等于每個線程都有一把獨自的鎖,自然也就不是所謂的同步了。
而同步函數的原理實際上與同步代碼塊是相同的,不同的只是將原本包含在同步代碼塊當中的代碼單獨封裝到一個函數中:
~~~
private synchronized void saleTicket() {
while (true) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num-- + "號票";
System.out.println(output);
} else {
break;
}
}
}
~~~
而另外一點值得說明的是,就是關于不同方式使用的鎖的差別:
同步代碼塊:可以使用任一對象鎖。
同步函數:使用this作為鎖。
靜態同步函數:使用該函數所在類的字節碼文件對象作為鎖。
# 死鎖現象
提到同步,就不得不提到與之相關的一個概念:死鎖。
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。
此時稱系統處于死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。同理,線程也會出現死鎖現象。
~~~
<span style="font-family:SimSun;font-size:12px;">package com.tsr.j2seoverstudy.thread;
public class DeadLockDemo {
public static void main(String[] args) {
Queue q1 = new Queue(true);
Queue q2 = new Queue(false);
Thread t1 = new Thread(q1, "線程1");
Thread t2 = new Thread(q2, "線程2");
t1.start();
t2.start();
}
}
class MyLocks {
public static final Object LOCK_A = new Object();
public static final Object LOCK_B = new Object();
}
class Queue implements Runnable {
boolean flag;
Queue(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (true) {
if (flag) {
synchronized (MyLocks.LOCK_A) {
System.out.println(threadName + "獲取了鎖A");
synchronized (MyLocks.LOCK_B) {
System.out.println(threadName + "獲取了鎖B");
}
}
} else {
synchronized (MyLocks.LOCK_B) {
System.out.println(threadName + "獲取了鎖B");
synchronized (MyLocks.LOCK_A) {
System.out.println(threadName + "獲取了鎖A");
}
}
}
}
}
}</span>
~~~
上面的程序就演示了一個死鎖的現象:

線程1開啟執行后,判斷標記為true,于是先獲取了鎖A,并輸出信息。
此時CPU做切換,線程2開啟執行,判斷標記為false,首先獲取鎖B,并輸出相關信息。
但這時候無論CPU再怎么樣切換,程序都已經無法繼續推進了。
因為線程1想要繼續推進必須獲取的資源鎖B現在被線程2持有,反之線程2需要的鎖A被線程1持有。
這正是因為兩個線程因為互相爭奪資源而造成的死鎖現象。
死鎖還是很蛋疼的,一旦出現,程序的調試和查錯修改工作都會變得很麻煩
# 線程通信 - 生產者與消費者的例子
關于多線程編程,類似于車站賣票的例子是一種常見的使用途徑。
這種應用途徑通常為:多個線程操作共享數據,并且執行的是同一個動作(線程任務)。
車站售票:多個線程都是操作同一組車票,并且都是執行同一個動作:出售車票。
那么在多線程當中的另一個經典例子:生產者與消費者,就描述的是另一種常見的應用途徑。
多個線程操作共享數據,但是不同的線程之間執行的是不同的動作(線程任務),這就是線程通信的使用。
不同線程間的通信應當怎么樣來完成,其手段是通過Object類當中提供的幾個相關方法:
- wait():在其他線程調用此對象的`notify()`方法或notifyAll()方法前,導致當前線程等待。
- notify():喚醒在此對象監視器上等待的單個(任一一個)線程。
- notifyAll():喚醒在此對象監視器上等待的所有線程。
首先,我們可能會思考的一點就是:既然是針對于線程之間相互通信的方法,為什么沒有被定義在線程類,反而被定義在了Object類當中。
因為這些方法事實上我們可以視作是線程監視器的方法,監視器其實就是鎖。
我們知道同步中的鎖,可以是任意的對象,那么既然是任一對象調用的方法,自然一定被定義在Object類中。
可以將所有使用同一個同步的線程視作被存儲在同一個線程池當中,而該同步的鎖就是該線程池的監視器。
由該監視器來調度對應線程池內的各個線程,從而達到線程通信的目的。
接下來就來看生產者與消費者的例子:
1.生產者生產商品;
2.消費者購買商品。
3.可能會同時存在多個生產者與多個消費者。
4.多個生產者中某個生產者生產一件商品,就暫停生產,并在多個消費者中通知一個消費者進行消費;
消費者消費掉商品后,停止消費,再通知任一一個生產者進行新的生產工作。
~~~
package com.tsr.j2seoverstudy.thread;
public class ThreadCommunication {
public static void main(String[] args) {
Queue q = new Queue();
Customer c = new Customer(q);
Producer p = new Producer(q);
Thread t1 = new Thread(c, "消費者1-");
Thread t2 = new Thread(c, "消費者2-");
Thread t3 = new Thread(p, "生產者1-");
Thread t4 = new Thread(p, "生產者2-");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Queue {
//當前商品數量是否為0
private boolean isEmpty = true;
//生產
public synchronized void put() {
String threadName = Thread.currentThread().getName();
//如果生產者線程進入,而現在還有剩余商品
while (!isEmpty) {
try {
wait();//則該生產者暫時等待,不進行生產
} catch (InterruptedException e) {
}
}
//否則則生產一件商品
isEmpty = false;
System.out.println(threadName + "生產了一件商品");
//喚醒阻塞的線程,通知消費者消費
this.notifyAll();
}
//消費
public synchronized void take() {
String threadName = Thread.currentThread().getName();
//消費者前來消費,如果此時沒有剩余商品
while (isEmpty) {
try {
wait();//則讓消費者先行等待
} catch (InterruptedException e) {
}
}
//否則則消費掉商品
isEmpty = true;
System.out.println(threadName + "消費了一件商品");
//通知生產者沒有商品了,起來繼續生產
this.notifyAll();
}
}
class Customer implements Runnable {
Queue q;
Customer(Queue q) {
this.q = q;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
q.take();
}
}
}
class Producer implements Runnable {
Queue q;
Producer(Queue q) {
this.q = q;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
q.put();
}
}
}
~~~
這就是對線程通信一個簡單的應用。而需要記住的是:關于線程的停止與喚醒都必須定義在同步中。
因為我們說過了,關于所謂的線程通信工作。實際上是通過監視器對象(也就是鎖),來完成對線程的停止或喚醒的操作的。
既然使用的是鎖,那么自然必須被定義在同步中。并且,必須確保互相通信的線程使用的是同一個鎖。
這是十分重要的,試想一下,如果試圖用線程池A的監視器鎖A去喚醒另一個線程池B內的某一個線程,這自然是辦不到的。
簡單解釋下,你可能已經注意到在上面的例子中,我是直接采用"wait()"和"notifyAll()"的方式來喚醒和阻塞線程的。
那么你應該明白這其實對應于隱式的"this.wait()"與"this.notifyAll()",而同時我們已經說過了:
在同步方法中,使用的鎖正是this。也就是說,在線程通信中,你可以將同步鎖this看做是一個線程池的對象監視器。
當某個線程執行到this.wait(),就代表它在該線程池內阻塞了。而通過this.notify()則可以喚醒阻塞在這個線程池上的線程。
而到了這里,另一值得一提的一點就是:
Thread類的sleep()方法和Object類的wait()方法都可以使當前線程掛起,而它們的不同之處在于:
1:sleep方法必須線程掛起的時間,超過指定時間,線程將自動從掛起中恢復。而wait方法可以指定時間,也可以不指定。
2:線程調用sleep方法會釋放Cpu的執行資格(也就是進入到non Runnable狀態),但不會釋放鎖;
而通過調用wait方法,線程即會釋放cpu的執行資格,同時也會釋放掉鎖。
# 線程通信的安全隱患
與之前說過的賣票用例一樣,對于線程通信的通信也應當小心謹慎,否則也可能會引發相關的錯誤。常見的問題例如:
一、使用notify而不是notifyAll喚醒線程可能會出現的問題
我在最初接觸多線程的時候,容易這樣考慮,既然想要達到的目的是:
生產者線程生產一件商品,則喚醒一個消費者線程。消費者進行消費,則喚醒一個生產者線程。
既然notify()方法用于喚醒單個線程,而notifyAll()用于喚醒所有線程,那使用notifyAll不是浪費效率嗎?
后來明白,很可惜的是,我們要做的是喚醒單個對方線程。而notify沒有這么強大。
它只是隨機的喚醒一個處于阻塞狀態下的線程,所以如果使用notify(),可能會看到如下的錯誤情況:

沒錯,操蛋,又出現了坑爹的死鎖。為什么出現這樣的情況呢?我們來分析一下:
- 我們創建的4個線程經調用start方法之后,都進入了可執行狀態,具備CPU執行資格。
- CPU隨機切換,首先賦予“生產者1”執行權,生產者1開始執行。
- 生產者1判斷isEmpty為true,執行一次生產任務。當執行notify方法時,當前還沒有任何可以喚醒的阻塞線程。
- 生產者1繼續while循環,判斷isEmpty為flase。執行wait,于是生產者1進入阻塞狀態。
執行到此,當前處于可執行狀態的線程為:生產者2、消費者1、消費者2
- CPU在剩下的3個可執行狀態中隨機切換到了生產者2,于是生產者2開始執行。
- 生產者2判斷isEmpty為false,執行wait方法,于是生產者2也進入到臨時阻塞狀態。
于是,當前處于可執行狀態的線程變為了:消費者1、消費者2
- CPU繼續隨機切換,此次切換到消費者1開始執行。
- 消費者1判斷isEmpty為false,于是執行一次消費,修改isEmpty為true。
- 執行到notify()方法,喚醒任一阻塞狀態的線程,于是喚醒了生產者2。
- 消費者1繼續while循環,判斷isEmpty為true,于是執行wait,進入阻塞。
到此,當前處于可執行狀態的線程變為了:生產者2、消費者2
- 同樣的,CPU這次切換到消費者2執行。
- 消費者2判斷isEmpty為true,于是執行wait,進入阻塞。
好了,處于可執行狀態的線程只剩下:生產者2。
- 那么,自然現在只能是輪到生產者2執行了。
- 判斷isEmpty為true,執行一次生產。修改isEmpty為false。
- 通過notify()方法隨機喚醒了生產者1線程。
- 再次執行while循環,判斷isEmpty為false后,進入阻塞。
至此,唯一處于可執行狀態的線程變為了:生產者1
- 生產者1線程開始執行。
- 判斷isEmpty為false,執行wait進入阻塞。
這下好了,4個線程都進入了阻塞狀態,而不是消亡狀態。自然的,死鎖了。
二、使用if而不是使用while判斷isEmpty可能出現的問題
如果使用if而不是while對isEmpty進行判斷,可能會出現的錯誤為:
1、不同的生產者連續生產了多件商品,但消費者只消費掉其中一件。
2、一個生產者生產了一件商品之后,有多個消費者進行連續消費。
出現這樣的安全問題是因為if的判斷機制造成的:通過if來判斷標記,只會執行一次判斷。
所以可能會導致不該運行的線程運行了,從而出現數據錯誤的情況。
這種問題的出現也就是與我們上面說的“售票處售出0號票”的錯誤類似。
# JDK1.5之后的新特性
我們前面已經說到了,關于生產者與消費者的問題中。
我們的目的是,每當一個線程執行完畢一次任務后,只喚醒單一的對方線程。
而在JDK1.5之前,為了避免死鎖的發生,我們不得不使用notifyAll()來喚醒線程。
而這樣做有一個缺點就在于:每次都要喚醒所有處于阻塞的線程,自然就會導致效率降低。
在JDK1.5之后,,Java提供了新的工具用于解決此類問題,就是:Lock和Condition接口。
簡答的說,就是對將原本的同步鎖synchronized與對象監視器進行了封裝,分別對應于于Lock及Condition。
并且,重要的是相對于1.5之前,新的工具擁有更靈活及更廣泛的操作。
一、Lock的使用及注意事項
1、通過Lock lock = ?new ReentrantLock();獲取一個Lock對象。
2、通過成員方法lock(),用于對代碼進行同步管理。
3、通過成員方法unlock(),用于同步代碼執行完畢后,釋放鎖對象。
4、由于不管在同步代碼的執行過程中是否出現異常,最后都必須釋放該鎖,否則可能會導致死鎖現象的產生。所以通常在使用lock時,都會遵循如下格式:
??? lock.lock();
??? try{
??? {
???? // 同步代碼....
??? }finally{
?? lock.unlock();
? ? }
??? }
二、對象監視器Condition的使用及注意事項
1、可以通過Lock對象使用成員方法newCondition()來獲取一個新的監視器對象。
2、Condition分別使用await();signal();signalAll()來替代原本Object類當中的wait();notify();及notifyAll()方法。
3、同一個Lock對象可以擁有多個不同的Condition對象。
請注意一個很關鍵的特性:同一個Lock對象可以擁有多個不同的Condition對象!
也就是說:通過此特性,我們可以獲取多個Condition對象,將操作不同線程任務的線程分別存放在不同的Condition對象當中。
例如在前面所說的生產者消費者例子當中,我們就可以生成兩組監視器,一組監視生產者線程,一組監視消費者線程。
從而達到我們想要的每次只喚醒對方線程而不喚醒本方線程的目的,修改后的例子代碼如下:
~~~
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadCommunication {
public static void main(String[] args) {
Queue q = new Queue();
Customer c = new Customer(q);
Producer p = new Producer(q);
Thread t1 = new Thread(c,"消費者1-");
Thread t2 = new Thread(c,"消費者2-");
Thread t3 = new Thread(p,"生產者1-");
Thread t4 = new Thread(p,"生產者2-");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Queue {
private int goodsTotal;
private boolean isEmpty = true;
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
public void put() {
String threadName = Thread.currentThread().getName();
lock.lock();
try{
while (!isEmpty) {
try {
notFull.await();
} catch (InterruptedException e) {
}
}
goodsTotal ++;
System.out.println(threadName + "生產了一件商品");
isEmpty = false;
notEmpty.signal();
}finally{
lock.unlock();
}
}
public synchronized void take() {
String threadName = Thread.currentThread().getName();
lock.lock();
try{
while (isEmpty) {
try {
notEmpty.await();
} catch (InterruptedException e) {
}
}
goodsTotal --;
System.out.println(threadName + "消費了一件商品");
isEmpty = true;
notFull.signal();
}finally{
lock.unlock();
}
}
}
class Customer implements Runnable {
Queue q;
Customer(Queue q) {
this.q = q;
}
@Override
public void run() {
while (true) {
q.take();
}
}
}
class Producer implements Runnable {
Queue q;
Producer(Queue q) {
this.q = q;
}
@Override
public void run() {
while (true) {
q.put();
}
}
}
~~~
# 線程的常用方法
最后,看一下一些關于線程的常用方法。
一、線程的中斷工作
1、通常使用自然中斷的做法,也就是當某個線程的線程任務執行結束之后,該線程就會自然終結。
2、通過標記控制。如果線程任務中存在循環(通常都有),那么,可以在循環中使用標記,通過標記來控制線程的中斷。
二`、interrupt()方法:中斷線程`
`我們知道sleep及wait等方法都可以使線程進入阻塞狀態。所以可能你在程序通過使用標記的方式來控制線程的中斷,但由于過程中線程陷入了`凍結(掛起/阻塞)狀態,這時通過標記將無法正常的控制線程中斷。這時,就可以通過interrupt方法來中斷線程的凍結狀態,強制恢復到運行狀態中來,讓線程具備cpu的執行資格。但是因為此方法具有強制性,所以會引發InterruptedException,所以要記得處理異常。
三、setDaemon()方法:將該線程標記為守護線程或用戶線程。
所謂守護線程,可以理解為后臺線程。對應的,我們在程序中開辟的線程都可以視為前臺線程,在Java中,當所有的前臺線程都執行結束之后,后臺線程也將隨之結束。
例如:你在某個程序中開辟兩個線程,一個用于接收輸入,一個用于控制輸出。因為只有當有輸入存在時,才會存在輸出。這時就可以通過setDaemon將輸出線程設置為守護線程。這樣當輸入線程中斷結束時,輸出線程就會隨之自動中斷,而不必再人為控制中斷。
四、控制線程優先級
所謂控制線程優先級,是指我們可以通過設置線程的優先級來控制線程被CPU運行到的幾率,線程的優先級越高,被CPU運行的概率越大。
通過setPriority()與getPriority()方法可以分別設置和獲取某個線程的優先級。Java中線程的優先級取值范圍為:1-10
Thread類中使用MAX_PRIORITY(10),NORM_PRIORITY(5),MIN_PRIORITY(1)三個常量代表最常用的線程優先級值。
五、join()方法
線程使用join方法,意味著該線程申請加入執行,所以通常如果要臨時加入一個線程,可以使用join()方法。并且,當執行到join方法之后,其余線程將等待使用該方法的線程執行完線程任務之后,再繼續執行。
六、yiled()方法
暫停正在執行的線程對象,并執行其他線程。
- 前言
- 第一個專欄《重走J2SE之路》,你是否和我有一樣的困擾?
- 磨刀不誤砍材工 - 環境搭建(為什么要配置環境變量)
- 磨刀不誤砍材工 - Java的基礎語言要素(定義良好的標示符)
- 磨刀不誤砍材工 - Java的基礎語言要素(關鍵字)
- 磨刀不誤砍材工 - Java的基礎語言要素(注釋-生成你自己的API說明文檔)
- 磨刀不誤砍材工 - Java的基礎語言要素(從變量/常量切入,看8種基本數據類型)
- 磨刀不誤砍材工 - Java的基礎語言要素(運算符和表達式的應用)
- 磨刀不誤砍材工 - Java的基礎語言要素(語句-深入理解)
- 磨刀不誤砍材工 - Java的基礎語言要素(數組)
- 換一個視角看事務 - 用&quot;Java語言&quot;寫&quot;作文&quot;
- 牛刀小試 - 淺析Java的繼承與動態綁定
- 牛刀小試 - 詳解Java中的接口與內部類的使用
- 牛刀小試 - 趣談Java中的異常處理
- 牛刀小試 - 詳解Java多線程
- 牛刀小試 - 淺析Java集合框架的使用
- 牛刀小試 - Java泛型程序設計
- 牛刀小試 - 詳細總結Java-IO流的使用