java筆記:熟練掌握線程技術—基礎篇之線程的協作和死結的問題(下)

來源:互聯網
上載者:User

  本文的主題是線程的協作和死結。

  線程的協作我個人覺得就是線程的通訊,比如有A和B兩個線程,A和B都可以獨立運行,A和B有時也會做資訊的交換,這就是線程的協作了。在java裡線程的協作是通過線程之間的“握手機制”進行的,這種握手機制是通過Object類裡的wait()和notify()來實現的

  在我的記憶裡,sleep(),wait()和notify()(notifyAll())方法是最愛被面試官問道的問題。下面我就從這幾個方法的關係開始說起最終引入到線程協作的問題。

  sleep()方法屬於Thread類,wait()和notify()(notifyAll())方法屬於Object類

  上面就是我要說的第一的原理,這裡我再強調下:sleep()方法屬於Thread類,wait()和notify()(notifyAll())方法屬於Object類

  我在前面文章講過,java裡的對象天生都包含一個鎖,鎖是屬於對象Object類而非Thread類,那麼這裡就又有一個原理了:調用sleep()的時候鎖並沒有被釋放。sleep()和wait()方法都可以讓線程停止,但是兩種方法停止的本質是不同的,wait()方法可以釋放鎖,而sleep()不能釋放鎖,這個特性很重要,鎖機制是保證安全執行緒的,實現線程同步,sleep()方法不能釋放鎖也就說明sleep()方法控制不了線程同步,而wait方法使用時候可以讓被鎖同步的其他的方法被調用,所以wait()方法能控制線程同步大家看到了,wait()方法的使用影響到了其他線程的使用,這就是所謂的線程的協作了,同樣的對於notify()和notifyAll()方法他們會喚醒等待的線程,就是讓線程獲得當前的對象鎖,而通知其他線程暫停下,這也是一種線程協作了

  總之等待(wait())和通知(notify())就是線程協作的一種方式了。

  Object類的wait()方法有兩種形式,第一種是接受毫秒數作為參數,這種用法的意義和sleep()方法裡的參數的意思相同,都是表達在“某個時間之內暫停”。但也有不同之處:一個就是上面釋放鎖的問題,第二個被wait()等待的線程可以通過notify()、notifyAll(),或者令時間到期從wait狀態中恢複過來。

  第二種用法wait()方法不帶任何參數,這種用法更常見。wait()使得線程無限的等待下去,直到這個線程收到notify()或者notifyAll()訊息。

  wait()、notify()和notifyAll()方法屬於Object類的一部分而不是Thread類的一部分,這個咋看一下真的很奇怪,不過細細思考下,這種做法是有道理的,我們把鎖機制授予對象成為對象密不可分的屬性會幫我們擴充線程應用的思路,至少把這幾個方法放到對象中,我們就可以把wait方法放到任何的具有同步控制功能的方法,而不用去考慮方法所在的類是繼承了Thread還是實現了Runnable介面。但是事實上使用sleep()、wait()、notify()和notifyAll()方法還是要注意:只能在同步控制方法或同步塊裡調用wait()、notify()和notifyAll()方法,因為這些操作都會使用到鎖;而對於不操作鎖的操作也就是非同步控制方法裡我們才能調用sleep()方法。下面就是我們常常在無意中會犯的問題:如果是在非同步的方法裡調用wait()、notify()和notifyAll()方法,程式會編譯通過,但是在運行時候程式會報出IllegalMonitorStateException異常,同時會包含一些讓人摸不著頭腦的提樣本如:當前線程不是擁有者,這個提示的含義是:調用wait()、notify()和notifyAll()方法的線程在調用這些方法前必須擁有這個對象的鎖

  要理解wait()、notify()和notifyAll()這三個方法,關鍵就在wait()方法,一般在什麼樣情況下我們使用wait()方法了,下面這段文字就是我自己對他的總結:

  當你編寫的帶有同步性質的線程們其中有個A線程,我們先讓A線程暫停了,當程式運行到某個時刻,我們又需要A線程啟動起來幹活,而此時的A線程在幹嘛呢?它在等待一個條件去啟用它,而啟用它的條件又必須是另外一個線程比如是B線程發出,那就應該使用wait方法讓A線程停止了,而想喚醒被wait方法停止的線程就得使用notify或者是notifyAll方法了

  下面我寫了一段代碼來示範wait和notify的用法:

package cn.com.sxia;

class Order{
private static int i = 0;
private int count = i++;

public Order(){
if (count == 10){
System.out.println("食物沒有了,打烊");
System.exit(0);
}
}

@Override
public String toString() {
return "Order [count=" + count + "]";
}
}

class WaitPerson extends Thread{
private Restaurant restaurant;

public WaitPerson(Restaurant r){
restaurant = r;
start();
}

public void run(){
while(true){
while(restaurant.order == null){
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("服務員得到訂單:" + restaurant.order);
restaurant.order = null;
}
}
}
}

class Chef extends Thread{
private Restaurant restaurant;
private WaitPerson waitPerson;

public Chef(Restaurant r,WaitPerson w){
restaurant = r;
waitPerson = w;
start();
}

public void run(){
while(true){
if (restaurant.order == null){
restaurant.order = new Order();
System.out.println("下訂單");
synchronized (waitPerson) {
waitPerson.notify();
}
}
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class Restaurant {

Order order;

public static void main(String[] args) {
Restaurant restaurant = new Restaurant();
WaitPerson waitPerson = new WaitPerson(restaurant);
Chef chef = new Chef(restaurant, waitPerson);

}

}

  結果如下:

下訂單
服務員得到訂單:Order [count=0]
下訂單
服務員得到訂單:Order [count=1]
下訂單
服務員得到訂單:Order [count=2]
下訂單
服務員得到訂單:Order [count=3]
下訂單
服務員得到訂單:Order [count=4]
下訂單
服務員得到訂單:Order [count=5]
下訂單
服務員得到訂單:Order [count=6]
下訂單
服務員得到訂單:Order [count=7]
下訂單
服務員得到訂單:Order [count=8]
下訂單
服務員得到訂單:Order [count=9]
食物沒有了,打烊

  程式的邏輯意思大致是這樣的:有一個餐館Restaurant,裡面只有一個廚師Chef和一個服務員WaitPerson,服務員必須等待廚師做好食物,而廚師做好了食物的時候會通知服務員食物做好了,服務員得到食物分給客人,接著服務員獲得訂單告訴廚師,服務員繼續等待,直到廚師的10個食物全部做完。

  這就是典型的線程協作的例子:生產者(廚師)—消費者()服務員。

  程式中,WaitPerson必須首先從Restaurant哪裡獲得訂單restaurant.order,接著在waitperson的run方法裡調用wait方法讓waitperson對象的線程處於等待狀態,直到chef對象使用notify方法喚醒他。因為我們寫的是簡單程式,所以我們知道一個WaitPerson等待被喚醒,如果有多個waitperson服務員再等待同一個鎖,那麼就得使用notifyAll方法了,notifyAll將喚醒所有等待該鎖的線程,而到底哪個線程對廚師的結果做出相應的回應就是要這些線程自己做協調了。至於Chef對象他必須知道餐館的訂單和要取走它食物的waitperson對象,這樣就保證了訂單的食物做好後會通知相關的服務員,大家看到了在Chef的run方法裡調用notify方法,這裡有個細節我要講講了:當Chef裡的run方法調用waitperson的notify方法時候,chef對象會獲得waitperson對象的鎖,而原來執行wait()的waitperson對象會釋放自己的鎖,而notify方法調用時候對鎖的控制有唯一性,這就保證了多個線程都有同一個對象的notify方法時候不會引起線程衝突。

  從這個例子表達的內容我們可以看出:一個線程操作了某個對象,另一個線程通過一定的方式可以使用前一個線程操作的對象,而這種典型的線程裡的“生產者-消費者”模式中,使用的是“先進先出”的隊列方式實現生產者和消費者模型

  Java裡的線程協作還有更多的方式,不過上面的方式是最常用的,至於其他的實現我現在的資料不全,等資料收集全面我會線上程的進階篇裡寫道。

  下面我要講死結了。講到死結之前首先要瞭解線程的狀態。

  線程的狀態一共分為5類:

  1. 建立(New):線程對象已經被建立,但是它還沒有被啟動,因此這個建立的好的線程還不能運行;
  2. 待運行(Runnable):在這種狀態下,只要線程的發送器把CPU計算的時間片分配給該線程,該線程就能運行了。這種狀態就和汽車空轉一樣,我們知道汽車已經啟動了,但它就是沒跑,只要我們稍微踩下油門,汽車馬上就可以飛奔起來。
  3. 運行(Running):線程已經啟動了,線程調度機制賦予了線程CPU運算時間,正在跑的狀態,線程正處在run方法運行之中;
  4. 死亡(Dead):線程的死亡通常都是因為run方法被跳出。
  5. 阻塞(Blocked):線程能夠運行,但是某個條件阻止了它的運行。當線程處在阻塞狀態下,線程的調度機制將會忽略該線程,不會再分配給該線程任何CPU計算時間,只有當該線程重新進入到待運行狀態時候,該線程才能執行。

  我們要關心的是阻塞狀態。那些原因能引起線程的阻塞呢?下面是我的總結:

  1. 當線程調用了sleep方法使得線程被掛起等待時候;
  2. 同樣線程使用wait方法時候也會阻塞線程;
  3. 線程打算在某個對象上調用其同步控制方法,但是該線程的鎖卻無法被使用。

  死結的問題就是線程阻塞和同步結合時候所產生的毛病。因為線程可以被阻塞,並且對象具有同步控制方法來防止別的線程在鎖沒有被釋放時候就訪問該對象,那麼當線程運行時候就會產生下面的問題:A在等待B執行完畢或者是等待B程通知自己被釋放的時候,而B線程又等待別的線程,這樣一直延伸下去,直到這個等待的線程鏈條上的某個線程又會等待第一個線程也就是A線程釋放掉自己的鎖,這就很像死迴圈了,線程之間相互等待互不相讓,最終所有線程都無法執行了,這就是“死結”

  談到線程的死結,就不得不提經典的死結現象:哲學家就餐問題。哲學家就餐問題是電腦界著名的科學家艾茲格·迪科斯徹提出的,原問題是:有5名哲學家,這些哲學家花部分時間思考問題,部分時間就餐,當他們思考時候,不需要任何共用資源,但是當他們就餐時候,但是他們的餐桌旁只有有限的餐具,如果餐桌的食物是麵條,那麼一個哲學家需要兩根筷子。但是哲學家們都很窮,因此他們只購買了5跟筷子,5跟筷子平分給5個哲學家,當一個哲學家就餐時候,該哲學家必須從他左邊或者右邊的哲學家裡借到一根筷子,當借到筷子的哲學家就餐時候,被借筷子的哲學家就得等待了

  下面就是哲學家問題的代碼:

package cn.com.sxia;

import java.util.Random;

class Chopstick{
private static int counter = 0;
private int number = counter++;

@Override
public String toString() {
return "Chopstick [number=" + number + "]";
}
}

class Philosopher extends Thread{
private static Random rand = new Random();
private static int counter = 0;
private int number = counter++;
private Chopstick leftChopstick;
private Chopstick rightChopstick;
static int ponder = 0;

public Philosopher(Chopstick left,Chopstick right){
leftChopstick = left;
rightChopstick = right;
start();
}

public void think(){
System.out.println(this + "正在思考");
if (ponder > 0){
try {
sleep(rand.nextInt(ponder));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void eat(){
synchronized (leftChopstick) {
System.out.println(this + "擁有" + this.leftChopstick + "正在等待" + rightChopstick);
synchronized (rightChopstick) {
System.out.println(this + "正在吃");
}
}
}

@Override
public String toString() {
return "Philosopher [number=" + number + "]";
}

public void run(){
while(true){
think();
eat();
}
}
}

public class DiningPhilosophers {

public static void main(String[] args) {
if (args.length > 3){
System.out.println("參數輸入的個數不正確");
System.exit(1);
}

Philosopher[] philosophers = new Philosopher[Integer.parseInt(args[0])];
Philosopher.ponder = Integer.parseInt(args[1]);
Chopstick left = new Chopstick(),right = new Chopstick(),first = left;
int i = 0;
while(i < philosophers.length - 1){
philosophers[i++] = new Philosopher(left, right);
left = right;
right = new Chopstick();
}
if (args[2].equals("deadlock")){
philosophers[i] = new Philosopher(left, first);
}else{
philosophers[i] = new Philosopher(first, left);
}

if (args.length >= 4){
int delay = Integer.parseInt(args[3]);
if (delay != 0){
//Timeout就是我在前面寫的逾時架構
new Timeout(delay * 1000, "逾時了");
}
}
}

}

  程式中Chopstick就是筷子和哲學家Philosopher都使用一個自動增加的static counter來給每個產生的對象做標示,每一個Philosopher哲學家對象都有一個對左邊和右邊的Chopstick對象的引用,筷子就是哲學家在就餐前的餐具。靜態變數ponder英文單詞的意思是思考標示哲學家花多少時間進行思考,如果我們傳入的值是0,那麼在think方法裡線程休眠的時間就由隨機產生的。而在eat方法裡哲學家通過同步控制獲得左邊筷子,如果筷子不可用,哲學家就會等待,下面就是我對哲學家問題的改變,讓哲學家要獲得兩根筷子才能吃東西,當哲學家獲得左邊筷子後再用同樣的方法擷取右邊的筷子,就餐完畢後先釋放右邊的筷子在釋放左邊的筷子。

  在main方法裡,我們要傳入參數,如果參數小於3個,程式會提示參數個數不正確,讓程式退出,如果參數多餘3個,假如第三個參數輸入的是數字為N,那麼在N秒後程式就會提示逾時,正確參數個數是3個,第一個參數用來指明哲學家的個數,第二個參數用來指定哲學家思考的時間,第三個參數有兩種用法一個就是上面說的逾時時間,一個就是填入deadlock,這個參數就會讓程式死結,比如我輸入3,20,deadlock參數,結果如下:

……………………….
Philosopher [number=0]正在吃
Philosopher [number=0]正在思考
Philosopher [number=0]擁有Chopstick [number=0]正在等待Chopstick [number=1]
Philosopher [number=0]正在吃
Philosopher [number=0]正在思考
Philosopher [number=2]擁有Chopstick [number=2]正在等待Chopstick [number=0]
Philosopher [number=2]正在吃
Philosopher [number=2]正在思考
Philosopher [number=0]擁有Chopstick [number=0]正在等待Chopstick [number=1]
Philosopher [number=1]擁有Chopstick [number=1]正在等待Chopstick [number=2]
Philosopher [number=2]擁有Chopstick [number=2]正在等待Chopstick [number=0]

  程式死結的原因是:最後一個Philosopher被給予了左邊筷子和前面存在在第一個first筷子,由於最後一個哲學家坐在第一個哲學家旁邊,他們共用了第一根筷子,在這種情況就會出現在某個時間點上所有哲學家都會準備就餐的情況,最終就會產生死結了

  死結是我們很不願意看到的現象,而且死結的問題很隱蔽,有時在程式交付客戶使用前都很難發現,讓客戶發現死結問題可能就是程式員最丟臉的時候了。要避免死結就得知道哪些情況會產生死結,產生死結一共需要4個條件被同時滿足:

  1. 線程使用的資源至少有一個不能被共用,比如上面的哲學家問題裡的筷子一次只能被一個哲學家使用,這種共用是排他的;
  2. 一個線程持有資源後還是處在等待狀態中,這個線程正等待另一個線程釋放相關的資源。
  3. 共用的資源不能被線程們搶佔,其實就是線程們一定要被同步;
  4. 線程之間必須有相互等待資源的情況,這個很像死迴圈了。

  要解決死結問題就是要破壞這四個條件中的一個即可,死結問題的產生很難從語言層級進行控制,它只能依靠程式員仔細設計自己的程式來避免,這是一個很麻煩的過程,但是也沒別的好辦法了。

  線程阻塞是產生很多問題的源頭,阻塞並不是錯誤,但有時因為線程阻塞碰到了錯誤我們很難糾正時候,那麼我們可以用很暴力的方法中斷線程,中斷了阻塞的線程也許我們程式裡的問題可能就被簡單解決了,但是這種方案的給人的體驗可能非常不好,想要中斷阻塞的線程我們可以使用Thread.interrupt()方法實現,大家看下面的代碼:

package cn.com.sxia;

import java.util.Timer;
import java.util.TimerTask;

class Blocked extends Thread{
public Blocked(){
System.out.println("線程阻塞開始了");
start();
}

public void run(){

try {
synchronized (this) {
wait();
}
} catch (InterruptedException e) {
System.out.println("中斷操作");
}
System.out.println("退出run方法");
}
}

public class Interrupt {

static Blocked blocked = new Blocked();
public static void main(String[] args) {
new Timer(true).schedule(new TimerTask() {

@Override
public void run() {
System.out.println("準備中斷線程");
blocked.interrupt();
blocked = null;
}
},2000);
}

}

  結果如下:

線程阻塞開始了
準備中斷線程
中斷操作
退出run方法

  線程的基礎篇講解完了,但是線程的主題還沒有結束,不過我所知道線程的基礎知識應該都講到了,對於線程我還會開啟一個線程進階篇,進階篇不是講解更難的東西,而是講一些有用或者有意思的線程技術。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.