Java多執行緒學習(吐血超詳細總結)

Evankaka發表於2015-03-14

         林炳文Evankaka原創作品。轉載請註明出處http://blog.csdn.net/evankaka

        寫在前面的話:此文只能說是java多執行緒的一個入門,其實Java裡頭執行緒完全可以寫一本書了,但是如果最基本的你都學掌握好,又怎麼能更上一個臺階呢?如果你覺得此文很簡單,那推薦你看看Java併發包的的執行緒池(Java併發程式設計與技術內幕:執行緒池深入理解),或者看這個專欄:Java併發程式設計與技術內幕。你將會對Java裡頭的高併發場景下的執行緒有更加深刻的理解。

目錄(?)[-]

  1. 一擴充套件javalangThread類
  2. 二實現javalangRunnable介面
  3. 三Thread和Runnable的區別
  4. 四執行緒狀態轉換
  5. 五執行緒排程
  6. 六常用函式說明
    1. 使用方式
    2. 為什麼要用join方法
  7. 七常見執行緒名詞解釋
  8. 八執行緒同步
  9. 九執行緒資料傳遞

        本文主要講了java中多執行緒的使用方法、執行緒同步、執行緒資料傳遞、執行緒狀態及相應的一些執行緒函式用法、概述等。在這之前,首先讓我們來了解下在作業系統中程式和執行緒的區別:

  程式:每個程式都有獨立的程式碼和資料空間(程式上下文),程式間的切換會有較大的開銷,一個程式包含1--n個執行緒。(程式是資源分配的最小單位)

  執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。(執行緒是cpu排程的最小單位)

  執行緒和程式一樣分為五個階段:建立、就緒、執行、阻塞、終止。

  多程式是指作業系統能同時執行多個任務(程式)。

  多執行緒是指在同一程式中有多個順序流在執行。

java中要想實現多執行緒,有兩種手段,一種是繼續Thread類,另外一種是實現Runable介面.(其實準確來講,應該有三種,還有一種是實現Callable介面,並與Future、執行緒池結合使用,此文這裡不講這個,有興趣看這裡Java併發程式設計與技術內幕:Callable、Future、FutureTask、CompletionService )

一、擴充套件java.lang.Thread類

這裡繼承Thread類的方法是比較常用的一種,如果說你只是想起一條執行緒。沒有什麼其它特殊的要求,那麼可以使用Thread.(筆者推薦使用Runable,後頭會說明為什麼)。下面來看一個簡單的例項

package com.multithread.learning;
/**
 *@functon 多執行緒學習
 *@author 林炳文
 *@time 2015.3.9
 */
class Thread1 extends Thread{
	private String name;
    public Thread1(String name) {
       this.name=name;
    }
	public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "執行  :  " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       
	}
}
public class Main {

	public static void main(String[] args) {
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();

	}

}
輸出:

A執行  :  0
B執行  :  0
A執行  :  1
A執行  :  2
A執行  :  3
A執行  :  4
B執行  :  1
B執行  :  2
B執行  :  3
B執行  :  4

再執行一下:

A執行  :  0
B執行  :  0
B執行  :  1
B執行  :  2
B執行  :  3
B執行  :  4
A執行  :  1
A執行  :  2
A執行  :  3
A執行  :  4
說明:
程式啟動執行main時候,java虛擬機器啟動一個程式,主執行緒main在main()呼叫時候被建立。隨著呼叫MitiSay的兩個物件的start方法,另外兩個執行緒也啟動了,這樣,整個應用就在多執行緒下執行。
 
注意:start()方法的呼叫後並不是立即執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),什麼時候執行是由作業系統決定的。
從程式執行的結果可以發現,多執行緒程式是亂序執行。因此,只有亂序執行的程式碼才有必要設計為多執行緒。
Thread.sleep()方法呼叫目的是不讓當前執行緒獨自霸佔該程式所獲取的CPU資源,以留出一定時間給其他執行緒執行的機會。
實際上所有的多執行緒程式碼執行順序都是不確定的,每次執行的結果都是隨機的。

但是start方法重複呼叫的話,會出現java.lang.IllegalThreadStateException異常。

		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=mTh1;
		mTh1.start();
		mTh2.start();

輸出:

Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Unknown Source)
    at com.multithread.learning.Main.main(Main.java:31)

A執行  :  0
A執行  :  1
A執行  :  2
A執行  :  3
A執行  :  4

二、實現java.lang.Runnable介面

採用Runnable也是非常常見的一種,我們只需要重寫run方法即可。下面也來看個例項。

/**
 *@functon 多執行緒學習
 *@author 林炳文
 *@time 2015.3.9
 */
package com.multithread.runnable;
class Thread2 implements Runnable{
	private String name;

	public Thread2(String name) {
		this.name=name;
	}

	@Override
	public void run() {
		  for (int i = 0; i < 5; i++) {
	            System.out.println(name + "執行  :  " + i);
	            try {
	            	Thread.sleep((int) Math.random() * 10);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
		
	}
	
}
public class Main {

	public static void main(String[] args) {
		new Thread(new Thread2("C")).start();
		new Thread(new Thread2("D")).start();
	}

}
輸出:

C執行  :  0
D執行  :  0
D執行  :  1
C執行  :  1
D執行  :  2
C執行  :  2
D執行  :  3
C執行  :  3
D執行  :  4
C執行  :  4

說明:
Thread2類通過實現Runnable介面,使得該類有了多執行緒類的特徵。run()方法是多執行緒程式的一個約定。所有的多執行緒程式碼都在run方法裡面。Thread類實際上也是實現了Runnable介面的類。
在啟動的多執行緒的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫Thread物件的start()方法來執行多執行緒程式碼。
實際上所有的多執行緒程式碼都是通過執行Thread的start()方法來執行的。因此,不管是擴充套件Thread類還是實現Runnable介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒程式設計的基礎。

三、Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。

總結:

實現Runnable介面比繼承Thread類所具有的優勢:

1):適合多個相同的程式程式碼的執行緒去處理同一個資源

2):可以避免java中的單繼承的限制

3):增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立

4):執行緒池只能放入實現Runable或callable類執行緒,不能直接放入繼承Thread的類


提醒一下大家:main方法其實也是一個執行緒。在java中所以的執行緒都是同時啟動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。

java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個jVM實習在就是在作業系統中啟動了一個程式。

四、執行緒狀態轉換

下面的這個圖非常重要!你如果看懂了這個圖,那麼對於多執行緒的理解將會更加深刻!


1、新建狀態(New):新建立了一個執行緒物件。
2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。
3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
(一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。(wait會釋放持有的鎖)
(二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
(三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

五、執行緒排程

執行緒的排程

1、調整執行緒優先順序:Java執行緒有優先順序,優先順序高的執行緒會獲得較多的執行機會。
 
Java執行緒的優先順序用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:
static int MAX_PRIORITY
          執行緒可以具有的最高優先順序,取值為10。
static int MIN_PRIORITY
          執行緒可以具有的最低優先順序,取值為1。
static int NORM_PRIORITY
          分配給執行緒的預設優先順序,取值為5。

Thread類的setPriority()和getPriority()方法分別用來設定和獲取執行緒的優先順序。
 每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。
執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。
JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式。
 
2、執行緒睡眠:Thread.sleep(long millis)方法,使執行緒轉到阻塞狀態。millis引數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。sleep()平臺移植性好。
 
3、執行緒等待:Object類中的wait()方法,導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價於呼叫 wait(0) 一樣。
 
4、執行緒讓步:Thread.yield() 方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。
 
5、執行緒加入:join()方法,等待其他執行緒終止。在當前執行緒中呼叫另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個程式執行結束,當前執行緒再由阻塞轉為就緒狀態。
 
6、執行緒喚醒:Object類中的notify()方法,喚醒在此物件監視器上等待的單個執行緒。如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過呼叫其中一個 wait 方法,在物件的監視器上等待。 直到當前的執行緒放棄此物件上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競爭;例如,喚醒的執行緒在作為鎖定此物件的下一個執行緒方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此物件監視器上等待的所有執行緒。
 注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因為有死鎖傾向。

六、常用函式說明

①sleep(long millis): 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)

②join():指等待t執行緒終止。

使用方式。

join是Thread類的一個方法,啟動執行緒後直接呼叫,即join()的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行。

Thread t = new AThread(); t.start(); t.join();

為什麼要用join()方法

在很多情況下,主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了。

不加join。
/**
 *@functon 多執行緒學習,join
 *@author 林炳文
 *@time 2015.3.9
 */
package com.multithread.join;
class Thread1 extends Thread{
	private String name;
    public Thread1(String name) {
    	super(name);
       this.name=name;
    }
	public void run() {
		System.out.println(Thread.currentThread().getName() + " 執行緒執行開始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子執行緒"+name + "執行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 執行緒執行結束!");
	}
}

public class Main {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主執行緒執行開始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");

	}

}



輸出結果:
main主執行緒執行開始!
main主執行緒執行結束!
B 執行緒執行開始!
子執行緒B執行 : 0
A 執行緒執行開始!
子執行緒A執行 : 0
子執行緒B執行 : 1
子執行緒A執行 : 1
子執行緒A執行 : 2
子執行緒A執行 : 3
子執行緒A執行 : 4
A 執行緒執行結束!
子執行緒B執行 : 2
子執行緒B執行 : 3
子執行緒B執行 : 4
B 執行緒執行結束!
發現主執行緒比子執行緒早結束

加join
public class Main {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主執行緒執行開始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		try {
			mTh1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			mTh2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");

	}

}

執行結果:
main主執行緒執行開始!
A 執行緒執行開始!
子執行緒A執行 : 0
B 執行緒執行開始!
子執行緒B執行 : 0
子執行緒A執行 : 1
子執行緒B執行 : 1
子執行緒A執行 : 2
子執行緒B執行 : 2
子執行緒A執行 : 3
子執行緒B執行 : 3
子執行緒A執行 : 4
子執行緒B執行 : 4
A 執行緒執行結束!
主執行緒一定會等子執行緒都結束了才結束

③yield():暫停當前正在執行的執行緒物件,並執行其他執行緒。
        Thread.yield()方法作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。
         yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
 
結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。可看上面的圖。
/**
 *@functon 多執行緒學習 yield
 *@author 林炳文
 *@time 2015.3.9
 */
package com.multithread.yield;
class ThreadYield extends Thread{
    public ThreadYield(String name) {
        super(name);
    }
 
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 當i為30時,該執行緒就會把CPU時間讓掉,讓其他或者自己的執行緒執行(也就是誰先搶到誰執行)
            if (i ==30) {
                this.yield();
            }
        }
	
}
}

public class Main {

	public static void main(String[] args) {
		
		ThreadYield yt1 = new ThreadYield("張三");
    	ThreadYield yt2 = new ThreadYield("李四");
        yt1.start();
        yt2.start();
	}

}

執行結果:

第一種情況:李四(執行緒)當執行到30時會CPU時間讓掉,這時張三(執行緒)搶到CPU時間並執行。

第二種情況:李四(執行緒)當執行到30時會CPU時間讓掉,這時李四(執行緒)搶到CPU時間並執行。

sleep()和yield()的區別
        sleep()和yield()的區別):sleep()使當前執行緒進入停滯狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
        sleep 方法使當前執行中的執行緒睡眼一段時間,進入不可執行狀態,這段時間的長短是由程式設定的,yield 方法使當前執行緒讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先順序的執行緒處於同可執行狀態,如有,則把 CPU  的佔有權交給此執行緒,否則,繼續執行原來的執行緒。所以yield()方法稱為“退讓”,它把執行機會讓給了同等優先順序的其他執行緒
       另外,sleep 方法允許較低優先順序的執行緒獲得執行機會,但 yield()  方法執行時,當前執行緒仍處在可執行狀態,所以,不可能讓出較低優先順序的執行緒些時獲得 CPU 佔有權。在一個執行系統中,如果較高優先順序的執行緒沒有呼叫 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先順序執行緒只能等待所有較高優先順序的執行緒執行結束,才有機會執行。

④setPriority(): 更改執行緒的優先順序。

    MIN_PRIORITY = 1
       NORM_PRIORITY = 5
           MAX_PRIORITY = 10

用法:
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

⑤interrupt():不要以為它是中斷某個執行緒!它只是線執行緒傳送一箇中斷訊號,讓執行緒在無限等待時(如死鎖時)能丟擲丟擲,從而結束執行緒,但是如果你吃掉了這個異常,那麼這個執行緒還是不會中斷的!

⑥wait()

Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){...}語句塊內。從功能上來說wait就是說執行緒在獲取物件鎖後,主動釋放物件鎖,同時本執行緒休眠。直到有其它執行緒呼叫物件的notify()喚醒該執行緒,才能繼續獲取物件鎖,並繼續執行。相應的notify()就是對物件鎖的喚醒操作。但有一點需要注意的是notify()呼叫後,並不是馬上就釋放物件鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()物件鎖的執行緒中隨機選取一執行緒,賦予其物件鎖,喚醒執行緒,繼續執行。這樣就提供了線上程間同步、喚醒的操作。Thread.sleep()與Object.wait()二者都可以暫停當前執行緒,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了物件鎖的控制。

    單單在概念上理解清楚了還不夠,需要在實際的例子中進行測試才能更好的理解。對Object.wait(),Object.notify()的應用最經典的例子,應該是三執行緒列印ABC的問題了吧,這是一道比較經典的面試題,題目要求如下:

    建立三個執行緒,A執行緒列印10次A,B執行緒列印10次B,C執行緒列印10次C,要求執行緒同時執行,交替列印10次ABC。這個問題用Object的wait(),notify()就可以很方便的解決。程式碼如下:

/**
 * wait用法
 * @author DreamSea 
 * @time 2015.3.9 
 */
package com.multithread.wait;
public class MyThreadPrinter2 implements Runnable {   
	  
    private String name;   
    private Object prev;   
    private Object self;   
  
    private MyThreadPrinter2(String name, Object prev, Object self) {   
        this.name = name;   
        this.prev = prev;   
        this.self = self;   
    }   
  
    @Override  
    public void run() {   
        int count = 10;   
        while (count > 0) {   
            synchronized (prev) {   
                synchronized (self) {   
                    System.out.print(name);   
                    count--;  
                    
                    self.notify();   
                }   
                try {   
                    prev.wait();   
                } catch (InterruptedException e) {   
                    e.printStackTrace();   
                }   
            }   
  
        }   
    }   
  
    public static void main(String[] args) throws Exception {   
        Object a = new Object();   
        Object b = new Object();   
        Object c = new Object();   
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   
           
           
        new Thread(pa).start();
        Thread.sleep(100);  //確保按順序A、B、C執行
        new Thread(pb).start();
        Thread.sleep(100);  
        new Thread(pc).start();   
        Thread.sleep(100);  
        }   
}  


輸出結果:

ABCABCABCABCABCABCABCABCABCABC

     先來解釋一下其整體思路,從大的方向上來講,該問題為三執行緒間的同步喚醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA迴圈執行三個執行緒。為了控制執行緒執行的順序,那麼就必須要確定喚醒、等待的順序,所以每一個執行緒必須同時持有兩個物件鎖,才能繼續執行。一個物件鎖是prev,就是前一個執行緒所持有的物件鎖。還有一個就是自身物件鎖。主要的思想就是,為了控制執行的順序,必須要先持有prev鎖,也就前一個執行緒要釋放自身物件鎖,再去申請自身物件鎖,兩者兼備時列印,之後首先呼叫self.notify()釋放自身物件鎖,喚醒下一個等待執行緒,再呼叫prev.wait()釋放prev物件鎖,終止當前執行緒,等待迴圈結束後再次被喚醒。執行上述程式碼,可以發現三個執行緒迴圈列印ABC,共10次。程式執行的主要過程就是A執行緒最先執行,持有C,A物件鎖,後釋放A,C鎖,喚醒B。執行緒B等待A鎖,再申請B鎖,後列印B,再釋放B,A鎖,喚醒C,執行緒C等待B鎖,再申請C鎖,後列印C,再釋放C,B鎖,喚醒A。看起來似乎沒什麼問題,但如果你仔細想一下,就會發現有問題,就是初始條件,三個執行緒按照A,B,C的順序來啟動,按照前面的思考,A喚醒B,B喚醒C,C再喚醒A。但是這種假設依賴於JVM中執行緒排程、執行的順序。
    wait和sleep區別
共同點:

1. 他們都是在多執行緒的環境下,都可以在程式的呼叫處阻塞指定的毫秒數,並返回。
2. wait()和sleep()都可以通過interrupt()方法 打斷執行緒的暫停狀態 ,從而使執行緒立刻丟擲InterruptedException。
   如果執行緒A希望立即結束執行緒B,則可以對執行緒B對應的Thread例項呼叫interrupt方法。如果此刻執行緒B正在wait/sleep /join,則執行緒B會立刻丟擲InterruptedException,在catch() {} 中直接return即可安全地結束執行緒。
   需要注意的是,InterruptedException是執行緒自己從內部丟擲的,並不是interrupt()方法丟擲的。對某一執行緒呼叫 interrupt()時,如果該執行緒正在執行普通的程式碼,那麼該執行緒根本就不會丟擲InterruptedException。但是,一旦該執行緒進入到 wait()/sleep()/join()後,就會立刻丟擲InterruptedException 。
不同點:
1. Thread類的方法:sleep(),yield()等
   Object的方法:wait()和notify()等
2. 每個物件都有一個鎖來控制同步訪問。Synchronized關鍵字可以和物件的鎖互動,來實現執行緒的同步。
   sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他執行緒可以使用同步控制塊或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用
所以sleep()和wait()方法的最大區別是:
    sleep()睡眠時,保持物件鎖,仍然佔有該鎖;
    而wait()睡眠時,釋放物件鎖。
  但是wait()和sleep()都可以通過interrupt()方法打斷執行緒的暫停狀態,從而使執行緒立刻丟擲InterruptedException(但不建議使用該方法)。

sleep()方法
sleep()使當前執行緒進入停滯狀態(阻塞當前執行緒),讓出CUP的使用、目的是不讓當前執行緒獨自霸佔該程式所獲的CPU資源,以留一定時間給其他執行緒執行的機會;
   sleep()是Thread類的Static(靜態)的方法;因此他不能改變物件的機鎖,所以當在一個Synchronized塊中呼叫Sleep()方法是,執行緒雖然休眠了,但是物件的機鎖並木有被釋放,其他執行緒無法訪問這個物件(即使睡著也持有物件鎖)。
  在sleep()休眠時間期滿後,該執行緒不一定會立即執行,這是因為其它執行緒可能正在執行而且沒有被排程為放棄執行,除非此執行緒具有更高的優先順序。
wait()方法
wait()方法是Object類裡的方法;當一個執行緒執行到wait()方法時,它就進入到一個和該物件相關的等待池中,同時失去(釋放)了物件的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還需要返還物件鎖);其他執行緒可以訪問;
  wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的執行緒。
  wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。

七、常見執行緒名詞解釋

主執行緒:JVM呼叫程式main()所產生的執行緒。
當前執行緒:這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的程式。
後臺執行緒:指為其他執行緒提供服務的執行緒,也稱為守護執行緒。JVM的垃圾回收執行緒就是一個後臺執行緒。使用者執行緒和守護執行緒的區別在於,是否等待主執行緒依賴於主執行緒結束而結束
前臺執行緒:是指接受後臺執行緒服務的執行緒,其實前臺後臺執行緒是聯絡在一起,就像傀儡和幕後操縱者一樣的關係。傀儡是前臺執行緒、幕後操縱者是後臺執行緒。由前臺執行緒建立的執行緒預設也是前臺執行緒。可以通過isDaemon()和setDaemon()方法來判斷和設定一個執行緒是否為後臺執行緒。
執行緒類的一些常用方法: 

  sleep(): 強迫一個執行緒睡眠N毫秒。 
  isAlive(): 判斷一個執行緒是否存活。 
  join(): 等待執行緒終止。 
  activeCount(): 程式中活躍的執行緒數。 
  enumerate(): 列舉程式中的執行緒。 
    currentThread(): 得到當前執行緒。 
  isDaemon(): 一個執行緒是否為守護執行緒。 
  setDaemon(): 設定一個執行緒為守護執行緒。(使用者執行緒和守護執行緒的區別在於,是否等待主執行緒依賴於主執行緒結束而結束) 
  setName(): 為執行緒設定一個名稱。 
  wait(): 強迫一個執行緒等待。 
  notify(): 通知一個執行緒繼續執行。 
  setPriority(): 設定一個執行緒的優先順序。

八、執行緒同步

1、synchronized關鍵字的作用域有二種:
1)是某個物件例項內,synchronized aMethod(){}可以防止多個執行緒同時訪問這個物件的synchronized方法(如果一個物件有多個synchronized方法,只要一個執行緒訪問了其中的一個synchronized方法,其它執行緒不能同時訪問這個物件中任何一個synchronized方法)。這時,不同的物件例項的synchronized方法是不相干擾的。也就是說,其它執行緒照樣可以同時訪問相同類的另一個物件例項中的synchronized方法;
2)是某個類的範圍,synchronized static aStaticMethod{}防止多個執行緒同時訪問這個類中的synchronized static 方法。它可以對類的所有物件例項起作用。

2、除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的作用域是當前物件;

3、synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法;

Java對多執行緒的支援與同步機制深受大家的喜愛,似乎看起來使用了synchronized關鍵字就可以輕鬆地解決多執行緒共享資料同步問題。到底如何?――還得對synchronized關鍵字的作用進行深入瞭解才可定論。

總的說來,synchronized關鍵字可以作為函式的修飾符,也可作為函式內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變數、object reference(物件引用)、static函式和class literals(類名稱字面常量)身上。

在進一步闡述之前,我們需要明確幾點:

A.無論synchronized關鍵字加在方法上還是物件上,它取得的鎖都是物件,而不是把一段程式碼或函式當作鎖――而且同步方法很可能還會被其他執行緒的物件訪問。

B.每個物件只有一個鎖(lock)與之相關聯。

C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

接著來討論synchronized用到不同地方對程式碼產生的影響:

假設P1、P2是同一個類的不同物件,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以呼叫它們。

1.  把synchronized當作函式修飾符時,示例程式碼如下:

Public synchronized void methodAAA()
{
//….
}

這也就是同步方法,那這時synchronized鎖定的是哪個物件呢?它鎖定的是呼叫這個同步方法物件。也就是說,當一個物件P1在不同的執行緒中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個物件所屬的Class所產生的另一物件P2卻可以任意呼叫這個被加了synchronized關鍵字的方法。

上邊的示例程式碼等同於如下程式碼:

public void methodAAA()
{
synchronized (this)      //  (1)
{
       //…..
}
}

 (1)處的this指的是什麼呢?它指的就是呼叫這個方法的物件,如P1。可見同步方法實質是將synchronized作用於object reference。――那個拿到了P1物件鎖的執行緒,才可以呼叫P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程式也可能在這種情形下襬脫同步機制的控制,造成資料混亂:(

2.同步塊,示例程式碼如下:

            public void method3(SomeObject so)
              {
                     synchronized(so)
{
       //…..
}
}

這時,鎖就是so這個物件,誰拿到這個鎖誰就可以執行它所控制的那段程式碼。當有一個明確的物件作為鎖時,就可以這樣寫程式,但當沒有明確的物件作為鎖,只是想讓一段程式碼同步時,可以建立一個特殊的instance變數(它得是一個物件)來充當鎖:

class Foo implements Runnable
{
       private byte[] lock = new byte[0];  // 特殊的instance變數
    Public void methodA()
{
       synchronized(lock) { //… }
}
//…..
}

注:零長度的byte陣列物件建立起來將比任何物件都經濟――檢視編譯後的位元組碼:生成零長度的byte[]物件只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3.將synchronized作用於static 函式,示例程式碼如下:

Class Foo
{
public synchronized static void methodAAA()   // 同步的static 函式
{
//….
}
public void methodBBB()
{
       synchronized(Foo.class)   //  class literal(類名稱字面常量)
}
       }

   程式碼中的methodBBB()方法是把class literal作為鎖的情況,它和同步的static函式產生的效果是一樣的,取得的鎖很特別,是當前呼叫這個方法的物件所屬的類(Class,而不再是由這個Class產生的某個具體物件了)。

記得在《Effective Java》一書中看到過將 Foo.class和 P1.getClass()用於作同步鎖還不一樣,不能用P1.getClass()來達到鎖這個Class的目的。P1指的是由Foo類產生的物件。

可以推斷:如果一個類中定義了一個synchronized的static函式A,也定義了一個synchronized 的instance函式B,那麼這個類的同一物件Obj在多執行緒中分別訪問A和B兩個方法時,不會構成同步,因為它們的鎖都不一樣。A方法的鎖是Obj這個物件,而B的鎖是Obj所屬的那個Class。

總結一下:

1、執行緒同步的目的是為了保護多個執行緒反問一個資源時對資源的破壞。
2、執行緒同步方法是通過鎖來實現,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他非同步方法
3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不干預。一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖。
4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的執行緒無法訪問競爭資源。
6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。
7、死鎖是執行緒間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程式,不一定好使,呵呵。但是,一旦程式發生死鎖,程式將死掉。

九、執行緒資料傳遞

在傳統的同步開發模式下,當我們呼叫一個函式時,通過這個函式的引數將資料傳入,並通過這個函式的返回值來返回最終的計算結果。但在多執行緒的非同步開發模式下,資料的傳遞和返回和同步開發模式有很大的區別。由於執行緒的執行和結束是不可預料的,因此,在傳遞和返回資料時就無法象函式一樣通過函式引數和return語句來返回資料。

9.1、通過構造方法傳遞資料 
在建立執行緒時,必須要建立一個Thread類的或其子類的例項。因此,我們不難想到在呼叫start方法之前通過執行緒類的構造方法將資料傳入執行緒。並將傳入的資料使用類變數儲存起來,以便執行緒使用(其實就是在run方法中使用)。下面的程式碼演示瞭如何通過構造方法來傳遞資料: 

 
package mythread; 
public class MyThread1 extends Thread 
{ 
private String name; 
public MyThread1(String name) 
{ 
this.name = name; 
} 
public void run() 
{ 
System.out.println("hello " + name); 
} 
public static void main(String[] args) 
{ 
Thread thread = new MyThread1("world"); 
thread.start(); 
} 
} 
由於這種方法是在建立執行緒物件的同時傳遞資料的,因此,線上程執行之前這些資料就就已經到位了,這樣就不會造成資料線上程執行後才傳入的現象。如果要傳遞更復雜的資料,可以使用集合、類等資料結構。使用構造方法來傳遞資料雖然比較安全,但如果要傳遞的資料比較多時,就會造成很多不便。由於Java沒有預設引數,要想實現類似預設引數的效果,就得使用過載,這樣不但使構造方法本身過於複雜,又會使構造方法在數量上大增。因此,要想避免這種情況,就得通過類方法或類變數來傳遞資料。 

9.2、通過變數和方法傳遞資料 
向物件中傳入資料一般有兩次機會,第一次機會是在建立物件時通過構造方法將資料傳入,另外一次機會就是在類中定義一系列的public的方法或變數(也可稱之為欄位)。然後在建立完物件後,通過物件例項逐個賦值。下面的程式碼是對MyThread1類的改版,使用了一個setName方法來設定 name變數: 

 
package mythread; 
public class MyThread2 implements Runnable 
{ 
private String name; 
public void setName(String name) 
{ 
this.name = name; 
} 
public void run() 
{ 
System.out.println("hello " + name); 
} 
public static void main(String[] args) 
{ 
MyThread2 myThread = new MyThread2(); 
myThread.setName("world"); 
Thread thread = new Thread(myThread); 
thread.start(); 
} 
} 
9.3、通過回撥函式傳遞資料 

上面討論的兩種向執行緒中傳遞資料的方法是最常用的。但這兩種方法都是main方法中主動將資料傳入執行緒類的。這對於執行緒來說,是被動接收這些資料的。然而,在有些應用中需要線上程執行的過程中動態地獲取資料,如在下面程式碼的run方法中產生了3個隨機數,然後通過Work類的process方法求這三個隨機數的和,並通過Data類的value將結果返回。從這個例子可以看出,在返回value之前,必須要得到三個隨機數。也就是說,這個 value是無法事先就傳入執行緒類的。 

 
package mythread; 
class Data 
{ 
public int value = 0; 
} 
class Work 
{ 
public void process(Data data, Integer numbers) 
{ 
for (int n : numbers) 
{ 
data.value += n; 
} 
} 
} 
public class MyThread3 extends Thread 
{ 
private Work work; 
public MyThread3(Work work) 
{ 
this.work = work; 
} 
public void run() 
{ 
java.util.Random random = new java.util.Random(); 
Data data = new Data(); 
int n1 = random.nextInt(1000); 
int n2 = random.nextInt(2000); 
int n3 = random.nextInt(3000); 
work.process(data, n1, n2, n3); // 使用回撥函式 
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 
+ String.valueOf(n3) + "=" + data.value); 
} 
public static void main(String[] args) 
{ 
Thread thread = new MyThread3(new Work()); 
thread.start(); 
} 
} 

  好了,Java多執行緒的基礎知識就講到這裡了,有興趣研究多執行緒的推薦直接看java的原始碼,你將會得到很大的提升!

林炳文Evankaka原創作品。轉載請註明出處http://blog.csdn.net/evankaka

相關文章