【java】詳解java多線程

目錄結構:java

contents structure [+]

在這篇Blog中,在這邊文章中,筆者將結合本身對多線程的理解,以及《java瘋狂講義》一書中多線程一章,對這篇文章作詳細的闡述。
都知道線程是進程的執行單元,一個進程能夠擁有多個線程,每一個線程都擁有獨立的棧堆,程序計數器。線程之間是獨立運行的,但他們之間也能夠相互通訊、相互影響。數據庫

1. 線程的建立與啓動

接下來介紹建立線程的三種方式,固然,方式遠不止這三種。編程

1.1 繼承Thread類建立線程類

這是一種比較常見的一種方式,經過繼承Thread類重寫其中的run()方法。
慄如:數組

public class ExtendsThreadTest extends Thread {
    private int i=0;
    public ExtendsThreadTest(String name) {
        super(name);
    }
    @Override
    public void run(){
        for(;i<100;i++){
            System.out.println(getName()+" -> "+i);
        }
    }
    public static void main(String[] args) {
        new ExtendsThreadTest("線程一").start();
        new ExtendsThreadTest("線程二").start();
    }
}

注:經過繼承Thread類的方法來建立線程類的時,多個線程之間沒法共享線程類的實例變量。緩存

1.2 實現Runnable接口建立線程類

在建立Thread類時,能夠指定一個Runnable參數,因此咱們能夠將Runnable接口的實現類傳給Thread類,以實現線程。安全

public class RunnableImpTest implements Runnable{
    public int i=0;
    @Override
    public void run() {
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" -> "+i);
        }
    }
    public static void main(String[] args) {
        RunnableImpTest runnableImp=new RunnableImpTest();
        new Thread(runnableImp,"線程一").start();
        
        new Thread(runnableImp,"線程二").start();
    }
}

注:經過實現Runnable接口,線程間能夠共享Runnable實現類的實例變量。多線程

1.3 使用Callable和Future建立線程

java5開始提供了一個Callable接口,Callable接口提供了一個call()方法做爲線程的執行體。call()方法比傳統的run()方法功能更強大,call()方法運行有返回值,容許拋出異常。

java5提供了Future接口做爲Callable接口裏call()方法的返回值,Future實現類提供了FutureTask實現類,FutureTask不只僅實現了Future接口,還實現了Runnable接口。該類能夠做爲線程的target使用。

併發

class CallableTest implements Callable<Boolean>{
    private int prime=0;
    public CallableTest(int prime){
        this.prime=prime;
    }
    @Override
    public Boolean call() throws Exception {
        //計算prime是不是素數
        if(prime<2){
            return false;
        }
        if(prime==2 || prime==3){
            return true;
        }
        for(int i=2;i<=Math.floor(Math.sqrt(prime));i++){
            if(prime%i==0){
                return false;
            }
        }
        return true;
    }
}
public class CallableThreadTest {
    
    public static void main(String[] args) {
        //建立Callable對象
        CallableTest callableTest=new CallableTest(15);
        
        //建立一個FutureTask任務
        FutureTask<Boolean> task=new FutureTask<Boolean>(callableTest);
        
        //啓動線程
        new Thread(task).start();
        
        //獲取線程的返回值
        try {
            System.out.println("is prime="+task.get());//一直阻塞,直到返回值
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("執行完畢");
    }
}

2. 線程的生命週期

當線程被建立啓動之後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態,在線程的生命週期中,它要通過新建(New),就緒(Runnable),運行(Running),阻塞(Blocked)和死亡(Dead)5中狀態。在線程啓動之後,它不可能一直「霸佔」着CPU獨自運行,線程的狀態也會在屢次運行、就緒之間切換。

新建,當程序使用New關鍵字建立一個線程後,該線程就處於新建狀態。此時,它和其餘的java對象同樣,僅僅由java虛擬機分配內存,並初始化成員變量的值。此時的線程對象並無表現出任何線程的動態特徵,程序也不會執行線程的線程執行體。

就緒,當線程調用了start()方法以後,該線程就處於就緒狀態。java會爲其建立方法調用棧和程序計數器,處於這個狀態的線程並無開始運行,只是表示該線程能夠運行了。

運行,若是處於就緒狀態的線程得到了CPU,開始執行run()方法的方法體,則該線程處於運行狀態。若是計算機只有一個CPU,那麼任什麼時候刻都只有一個線程處於運行。若是是在多處理器上,將會有多個線程並行執行。若是當前線程調用了yield()方法,那麼線程將會從新進行就緒狀態。

阻塞,當一個線程開始運行後,它不可能一直處於運行狀態(除非它的線程執行體足夠短,瞬間就被執行結束了),線程在運行的過程當中須要被中斷(阻塞),目的是使其餘線程得到執行的機會。被阻塞的線程會在合適的時候從新進入就緒狀態。

死亡,通常狀況下,當線程方法體執行結束後,線程結束;線程拋出未捕獲的異常,線程結束;線程調用stop()等終止線程的方法,線程結束。

線程生命週期以下圖所示:
dom

3. 控制線程

java中的線程提供了一些快捷的工具方法,經過這些工具能夠很好的控制線程。接下來介紹一些工具的使用。異步

3.1 join線程

Thread提供了讓一個線程等待另外一個線程完成的方法-join方法。當某個程序執行流中調用其餘線程的join()方法時,調用線程將被阻塞,直到被join()方法加入的join線程執行完畢,當前線程纔會從新回到就緒狀態。
join()方法有以下三種重載形式:
join():等待被join的線程執行完畢
join(long millis):等待被join的線程的時間最長爲millis毫秒。若是在millis毫秒內被join()的線程尚未執行結束,則再也不等待。
join(long millis,int nanos):等待被join的線程最長爲millis豪秒,nanos豪微秒。

class JoinThread extends Thread{
    public JoinThread(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i=0;i<100;i++){
            try{
                Thread.sleep(200);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(getName()+"->"+i);
        }
    }
}
public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        JoinThread joinThread = new JoinThread("被Join的線程");
        joinThread.start();
        joinThread.join();
        System.out.println("執行完畢");//在joinThread線程執行完畢後,纔會繼續執行。
    }
}

3.2 後臺線程

有一種線程,它是在後臺運行的,它的任務是爲其餘線程提供服務,這種線程被稱爲「後臺線程(Daemon Thread)」,又稱爲「守護線程」或「精靈線程」。JVM的垃圾回收線程就是典型的後臺線程。
後臺線程的特徵:若是全部的前臺線程都死亡了,後臺線程也會自動死亡。

調用Thread對象的setDaemon(true)可將制定線程設置爲後臺線程。下面的程序將執行線程設置爲後臺線程,全部的前臺線程都死亡時,後天線程也死亡,程序就退出了。

class DaemonThread extends Thread{
    public DaemonThread(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i=0;i<100;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+"->"+i);
        }
    }
}
public class DaemonThreadTest {
    
    public static void main(String[] args) {
        DaemonThread daemonThread= new DaemonThread("後臺線程");
        daemonThread.setDaemon(true);//設置爲後臺線程
        daemonThread.start();
        try {
            Thread.sleep(5000);//睡眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行完畢");
    }
}

從結果上面的結果能夠看出,前臺線程執行完畢後,後臺線程也就結束了。

3.3 線程睡眠:sleep

若是須要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則能夠經過調用Thread類的靜態sleep()方法來實現。噹噹前線程調用sleep()方法進入阻塞狀態後,在其睡眠時間內,該線程不會得到執行的機會,即便系統中沒有其它可執行的線程,處於sleep()的線程也不會執行,sleep()是用來暫停線程的執行。
在上面的案例中,已經展現了sleep()方法的使用了。

3.4 線程讓步:yield

yield()方法是一個和sleep()方法有點類似的方法,它也是Thread類提供的一個靜態方法。它也可讓當前正在執行的線程暫停,但它不會阻塞該線程,只是將該線程轉入就緒狀態。yeild()只是讓當前線程暫停一下,讓系統的線程調度器從新調度一次,徹底可能的狀況是:當某個線程調用了yield()線程暫停以後,線程調度器又將其調度出來從新執行。

當某個線程調用了yield()方法暫停以後,只有優先級與當前線程相同,或者優先級比當前線程更高的處於就緒狀態的線程纔會得到執行機會。
栗子:

class Yield implements Runnable{
    int i=0;
    @Override
    public void run(){
        for(;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"->"+i);
            //當i等於20時,當前線程讓步,讓線程調度器從新調度
            if(i==20){
                Thread.yield();
            }
        }
    }
}
public class YieldThreadTest extends Thread{
    public YieldThreadTest(Runnable runnable,String name){
        super(runnable,name);
    }
    public static void main(String[] args) {
        Yield yd=new Yield();
        
        YieldThreadTest ytt1=new YieldThreadTest(yd,"高級");
        ytt1.setPriority(Thread.MAX_PRIORITY);//設置優先級最高
        ytt1.start();
        
        YieldThreadTest ytt2=new YieldThreadTest(yd,"低級");
        ytt2.setPriority(Thread.MIN_PRIORITY);//設置優先級最低
        ytt2.start();
    }
}

運行上面的程序會發現,在通常狀況下會發現,「高級」和「低級」線程是交叉執行的,這是由於多CPU的緣由。若是用戶的計算機是單核的,那麼就能夠清楚看到上面的運行效果。

yield()方法和sleep()的區別以下:
1.sleep()方法暫停當前線程後,會給其餘線程執行機會,不會理會其餘線程的優先級;但yield()只會給優先級相同,或優先級更高的線程執行機會。
2.sleep()方法會將線程轉入阻塞狀態,直到通過阻塞時間纔會轉入就緒狀態;而yield()不會講線程轉入阻塞狀態,它只是將當前線程進入就緒狀態。
3.sleep()方法的聲明拋出了InterruptedException異常,因此調用sleep()方法時要麼捕捉改異常,要麼拋出該異常。
4.sleep()方法比yield()方法具備更好的可移動性,因此建議不要使用yield()方法來控制併發線程的執行。

3.5 改變線程優先級

每一個線程執行時都具備必定的優先級,優先級高的線程具備更高的執行機會,優先級低的線程具備更少的執行機會。每一個線程默認的優先級都與建立它的父級優先級相同。在默認狀況下,main線程具備普通優先級。

Thread對象提供了setPrority(int newPrority)、getPrority()來設置和返回優先級。其中setPrority的參數是一個int類型的整數,Thread類提供以下三個靜態常量MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY,顧名思義分別是最高、普通、最低優先級。
該方法在上面的案例中以及使用過了,這裏再也不贅述。

3.6 終止線程

3.6.1 使用退出標誌終止線程

當run方法執行完後,線程就會退出。但有時run方法是永遠不會結束的。如在服務端程序中使用線程進行監聽客戶端請求,或是其餘的須要循環處理的任務。在這種狀況下,通常是將這些任務放在一個循環中,如while循環。若是想讓循環永遠運行下去,可使用while(true){……}來處理。但要想使while循環在某一特定條件下退出,最直接的方法就是設一個boolean類型的標誌,並經過設置這個標誌爲true或false來控制while循環是否退出。下面給出了一個利用退出標誌終止線程的例子。

    public class ThreadFlag extends Thread  
    {  
        public volatile boolean exit = false;  
      
        public void run()  
        {  
            while (!exit);  
        }  
        public static void main(String[] args) throws Exception  
        {  
            ThreadFlag thread = new ThreadFlag();  
            thread.start();  
            sleep(5000); // 主線程延遲5秒  
            thread.exit = true;  // 終止線程thread  
            thread.join();  
            System.out.println("線程退出!");  
        }  
    }

在上面代碼中定義了一個退出標誌exit,當exit爲true時,while循環退出,exit的默認值爲false.在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit的值,

3.6.2 使用stop強行終止線程

 使用stop方法能夠強行終止正在運行或掛起的線程。咱們可使用以下的代碼來終止線程:
thread.stop();  

雖然使用上面的代碼能夠終止線程,但使用stop方法是很危險的,就象忽然關閉計算機電源,而不是按正常程序關機同樣,可能會產生不可預料的結果,所以,並不推薦使用stop方法來終止線程。

3.6.3 使用interrupt終止線程

使用interrupt方法來終端線程可分爲兩種狀況:
(1)線程處於阻塞狀態,如使用了sleep方法。
(2)使用while(!isInterrupted()){……}來判斷線程是否被中斷。
在第一種狀況下使用interrupt方法,sleep方法將拋出一個InterruptedException例外,而在第二種狀況下線程將直接退出。

下面的代碼演示了在第一種狀況下使用interrupt方法。

public class ThreadInterrupt extends Thread  
{  
    public void run()  
    {  
        try  
        {  
            sleep(50000);  // 延遲50秒  
        }  
        catch (InterruptedException e)  
        {  
            System.out.println(e.getMessage());  
        }  
    }  
    public static void main(String[] args) throws Exception  
    {  
        Thread thread = new ThreadInterrupt();  
        thread.start();  
        System.out.println("在50秒以內按任意鍵中斷線程!");  
        System.in.read();  
        thread.interrupt();  
        thread.join();  
        System.out.println("線程已經退出!");  
    }  
}

上面代碼的運行結果以下:

    在50秒以內按任意鍵中斷線程!  

    sleep interrupted  
    線程已經退出!   

在調用interrupt方法後, sleep方法拋出異常,而後輸出錯誤信息:sleep interrupted.

注意:在Thread類中有兩個方法能夠判斷線程是否經過interrupt方法被終止。一個是靜態的方法interrupted(),一個是非靜態的方法isInterrupted(),這兩個方法的區別是interrupted用來判斷當前線是否被中斷,而isInterrupted能夠用來判斷其餘線程是否被中斷。所以,while (!isInterrupted())也能夠換成while (!Thread.interrupted())。

4. 線程同步

4.1 概述

關於線程同步的知識,不少都是關於對象的。這裏筆者從另外一個角度來嘗試解釋線程同步,首先給出以下結論:

線程同步問題是指線程訪問線程做用域以外的資源引起的資源混亂問題(這裏的資源不只僅是對象,靜態字段等等。)

看看以下這個栗子:

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestMethod(0);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestMethod(10);
            }
        }).start();
    }
    public static void TestMethod(int i){
        System.out.println(Thread.currentThread().getName()+">i="+ i);
        try {
            Thread.sleep(1000);//休眠一秒鐘
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+">i="+(++i));
    }

兩個線程同時訪問TestMethod方法,而且傳入了不一樣的參數,在TestMethod上會引起線程同步的問題嗎?答案是不會。雖然兩個線程都訪問了TestMethod方法,可是在TestMethod方法中,並無訪問在該方法做用域以外的任何資源,變量i一直都在TestMethod方法的做用域以內。因此這裏不會引起線程同步的問題。

結果爲:

Thread-0>i=0
Thread-1>i=10
Thread-0>i=1
Thread-1>i=11

其實上面的代碼能夠看作以下這樣,就更好理解了。

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                System.out.println(Thread.currentThread().getName()+">i="+ i);
                try {
                    Thread.sleep(1000);//休眠一秒鐘
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+">i="+(++i));
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=10;
                System.out.println(Thread.currentThread().getName()+">i="+ i);
                try {
                    Thread.sleep(1000);//休眠一秒鐘
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+">i="+(++i));
            }
        }).start();
    }

若是把上面的代碼修改成以下代碼,則就會引起線程問題了。

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestMethod(0);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestMethod(10);
            }
        }).start();
    }
    static int c=-1;
    public static void TestMethod(int i){
        c=i;
        System.out.println(Thread.currentThread().getName()+">c="+ c);
        try {
            Thread.sleep(1000);//休眠一秒鐘
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+">c="+(++c));
    }

輸出以下:

Thread-0>c=0
Thread-1>c=10
Thread-0>c=11
Thread-1>c=12

上面筆者介紹了一下,在什麼狀況下會引起線程同步問題。在項目中,若是線程訪問的資源超過了它的做用域,那麼就應該考慮是線程同步了,java多線程中引入了同步監視器來進行線程同步。

4.2 同步鎖(synchronized)

synchronized有兩種用法,它便可做爲做爲同步代碼塊,也能夠同步方法。
同步代碼塊的語法是:

synchronized(obj){
    ...
}

格式中synchronized後括號裏的obj就是同步監視器,線程開始執行時,必須先得到對同步監視器的鎖定。
雖然java程序運行任何對象做爲同步監視器,但同步監視器的目的是爲了阻止多線程對共享資源的併發訪問,一般推薦使用可能被併發訪問的共享資源才做爲同步監視器。

同步方法,就是使用synchronized來修飾某個方法,則該方法被稱爲同步方法。對於Synchronized修飾的實例方法(非static方法,synchronized修飾static方法沒什麼意義,由於static最終是由類調用,跟線程對象無關。),無須顯式指定同步監視器,同步方法的監視器就是this,也就是調用該方法的特徵。
同步方法的語法是:

public synchronized void testMethod(){
    ...
}

下面使用synchronized同步代碼塊來模擬銀行取錢:

class Account{
    private String name=null;
    private Integer amount=0;
    
    public Account(String name,Integer amount){
        this.name=name;
        this.amount=amount;
    }
    
    public void Draw(int drawAmount){
        synchronized (amount) {//使用同步鎖鎖住amount
            System.out.println(name+" 開始取錢");
            if(amount>=drawAmount){//判斷
                amount-=drawAmount;//取錢
                System.out.println(name+" 帳戶餘額:"+amount);
            }else{
                System.out.println(name+" 餘額不足");
            }
        }
    }
}
public class DrawThread extends Thread{
    private Account account=null;
    private int amount=0;
    public DrawThread (Account account,int amount){
        this.account=account;
        this.amount=amount;
    }
    @Override
    public void run(){
        account.Draw(amount);
    }
    
    public static void main(String[] args) {
        Account account=new Account("富人甲", 1000);
        new DrawThread(account, 800).start();
        new DrawThread(account, 800).start();
    }
}

使用synchronized修飾同步代碼塊或是同步方法,都是爲了達到線程同步。若是有兩個線程須要同時訪問同一個字段,那麼可使用volatile來修改該字段。

4.3 同步鎖(Lock)

java5開始,提供了功能更強大的的線程同步機制-經過顯式定義同步鎖對象來實現同步,這種機制下,同步鎖由Lock對象充當,Lock比synchronized更靈活。
Lock和ReadLock是java5提供的兩個根接口,而且爲Lock提供了ReentrantLock實現類,爲ReadWriteLock提供了Reentrant實現類,爲ReadWriteLock提供了ReentrantReadWriteLock實現類。
ReadLock的語法格式爲:

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...
   public void m() {
     try {
     lock.lock();  // block until condition holds
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

使用Lock的時候,強烈建議使用try{}finally{}的格式,而且在try塊中加鎖,在finally塊中顯示釋放鎖。這樣的話,即便try中發生未捕獲的異常,那麼也能夠釋放鎖對象。
接下來使用ReenLock來重從上面銀行取錢的Account類。

class Account{
    //定義鎖
    private final ReentrantLock lock = new ReentrantLock();
    private String name=null;
    private Integer amount=0;
    
    public Account(String name,Integer amount){
        this.name=name;
        this.amount=amount;
    }
    
    public void Draw(int drawAmount){
        try{
            lock.lock();
            System.out.println(name+" 開始取錢");
            if(amount>=drawAmount){//判斷
                amount-=drawAmount;//取錢
                System.out.println(name+" 帳戶餘額:"+amount);
            }else{
                System.out.println(name+" 餘額不足");
            }
        }finally{
            lock.unlock();
        }
    }
}

4.4 死鎖

當兩個鎖相互等待對方釋放同步監視器時就會發生死鎖,java虛擬機沒有提供檢測,也沒有采起任何措施來處理死鎖的狀況,因此多線程編程中,應該採起措施避免死鎖。一旦出現死鎖,整個程序既不會發生任何錯誤,也不會給出任何提示,只是全部線程處於阻塞狀態,沒法繼續。
例如:

abstract class Car{
    protected String name=null;
    protected Car(String name){
        this.name=name;
    }
    
    public synchronized void entrySingleRoadWith(Car car){
        System.out.println(name+" 進入道路");
        try {
            Thread.sleep(1000);//睡眠一秒鐘
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(car.name+" 準備釋放道路全部權");
        car.release();
    }
    //釋放道路全部權,在釋放以前必需要得到對象同步鎖。
    public synchronized void release(){
        System.out.println(name+" 釋放道路全部權");
    }
}

class CarA extends Car{
    public CarA() {
        super("carA");
    }

}

class CarB extends Car{
    public CarB() {
        super("carB");
    }    
}

public class DeadLockTest extends Thread{
    private Car car1=null;
    private Car car2=null;
    public DeadLockTest(Car car1,Car car2){
        this.car1=car1;
        this.car2=car2;
    }
    @Override
    public void run() {
        car1.entrySingleRoadWith(car2);//car1和car2同時進入道路
    }
    public static void main(String[] args) {
        //建立兩個Car對象
        CarA carA=new CarA();
        CarB carB=new CarB();
        
        //建立兩個線程,而且開始運行
        new DeadLockTest(carA,carB).start();
        new DeadLockTest(carB,carA).start();
    }
}

5. 線程通訊

當線程在系統內運行時候,線程的調度具備必定的透明性,程序一般沒法準確控制線程的輪換執行。但java中提供了一些機制,來保證線程的協調運行。

線程通訊類的問題能夠按照以下的思路進行思考:
a.肯定臨界資源
b.肯定須要哪些線程類
c.肯定線程的通訊邏輯
d.肯定線程的退出條件

5.1 使用wait和notify控制線程通訊

Object類提供了wait()、notify()和notifyAll(),能夠經過調用Object對象的這三個方法來實現線程的通訊。
wait():致使當前線程等待,知道其餘線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程。
notify():喚醒在此同步監視器上等待的單個線程。若是有多個線程都在此同步監視器上等待,則只喚醒其中一個。
notifyAll():喚醒在此同步監視器上等待的全部線程。

若是使用synchronized修飾的同步方法,那麼該類的默認實例(this)就是同步監視器,因此能夠在同步方法中直接調用這三個方法。
若是使用synchronized修飾的是同步代碼塊,同步監視器是synchronized後括號裏的對象,因此必須使用該對象調用這三個方法。

下面的栗子是一個生產者-消費者的模型:

safeStack.java

public class SafeStack {
    /**
     * 下標
     */
    private int top=0;
    
    /**
     * 存儲產生的隨機整數
     */
    private int[] values=new int[10];
    
      /*
       * 壓棧和出棧的標誌,經過dataAvailable變量控制push()方法和pop()方法中線程的等待。
       * dataAvailable的值默認是false,最開始讓pop()方法中線程中等待。
       */
    private boolean dataAvailable=false;
    
    /**
     * 入棧
     */
    public synchronized void push(int val){
        if(dataAvailable){
            try{
                wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        values[top]=val;
        System.out.println("壓入數字"+val+"完成");
        top++;//入棧完成
        if(top>=values.length-1){//當values數組滿後,才改變狀態。
            dataAvailable=true;//狀態變爲出棧
            notifyAll();//喚醒線程
        }
    }
    /**
     * 出棧
     */
    public synchronized int pop(){
        if(!dataAvailable){
            try{
                wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        int res=values[top];
        System.out.println("彈出數字"+res+"完成");
        top--;
        if(top<=0){
            dataAvailable=false;
            notifyAll();
        }
        return res;
    }
}
SafeStack .java

PushThread.java

public class PushThread extends Thread{
    private SafeStack safeStack=null;
    public PushThread(SafeStack safeStack){
        super();
        this.safeStack=safeStack;
    }
    @Override
    public void run() {
        while(true){//假設一個生產者生產次數無限生產;也能夠指定生產次數。
            //得到隨機數
            int randomInt=new Random().nextInt(100);
            safeStack.push(randomInt);
        }
    }
}
PushThread.java

PopThread.java

public class PopThread extends Thread{
    private SafeStack safeStack=null;
    
    public PopThread(SafeStack safeStack){
        super();
        this.safeStack=safeStack;
    }
    
    @Override
    public void run(){
        while(true){//假設一個消費者,無限消費;也能夠指定消費次數。
             int value=safeStack.pop();
        }
    }
}
PopThread.java

測試類:

public class TestSafeStack {
    public static void main(String[] args) {
        SafeStack safeStack=new SafeStack();
        PushThread pushThread=new PushThread(safeStack);//建立了一個生產者
        PopThread popThread=new PopThread(safeStack);//建立消費者
        pushThread.start();
        popThread.start();
    }
}

5.2 使用Condition控制線程通訊

若是程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象阿里保證同步,則系統中不存在隱式的同步監視器,也就不能使用wait()、notify()和notifyAll()方法來保證線程間的通訊了。

當使用Lock對象來保證同步時,java提供了一個Condition類保持協調,使用Condition可讓那些已經獲得Lock對象卻沒法執行的線程釋放Lock對象,Condition對象也能夠喚醒其它處於等待的線程。

當Lock與Condition聯合使用時,Lock代替了同步代碼塊或是同步方法,Condition表明了同步監視器的功能。Condition的實例被綁定在Lock對象上,要得到指定Lock實例的Condition實例,調用Lock對象的newCondition()方法便可。

Condition類提供了以下三個方法:
await():相似於隱式同步監視器的上的wait()方法,致使當前線程等待,直到其餘線程調用該Condition的signal()方法或signalAll()方法來喚醒該線程。該await()方法有更多的變體,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,能夠完成更豐富的功能。
signal():喚醒在此Lock對象上等待的單個線程。若是有多個線程在該Lock對象上等待,則會選擇任意喚醒其中一個線程。
signalAll():喚醒在此Lock對象上等待的全部線程。只有當前線程放棄對該Lock對象的鎖定後,才能夠執行被喚醒的線程。

接下來,使用Condition來重寫上面的生產者-消費者案例中的臨界資源類SafeStack:

public class SafeStack {
    //顯示定義Lock對象
    private final Lock lock=new ReentrantLock();
    //得到Lock對象上的Condition
    private final Condition cond=lock.newCondition();
    /**
     * 下標
     */
    private int top=0;
    
    /**
     * 存儲產生的隨機整數
     */
    private int[] values=new int[10];
    
      /*
       * 壓棧和出棧的標誌,經過dataAvailable變量控制push()方法和pop()方法中線程的等待。
       * dataAvailable的值默認是false,最開始讓pop()方法中線程中等待。
       */
    private boolean dataAvailable=false;
    
    /**
     * 入棧
     */
    public void push(int val){
        try{
            lock.lock();//加鎖
            
            if(dataAvailable){
                try{
                    cond.await();//等待
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            values[top]=val;
            System.out.println("壓入數字"+val+"完成");
            top++;//入棧完成
            if(top>=values.length-1){//當values數組滿後,才改變狀態。
                dataAvailable=true;//狀態變爲出棧
                cond.signalAll();//喚醒其餘線程
            }            
        }finally{
            lock.unlock();//釋放鎖
        }
    }
    /**
     * 出棧
     */
    public int pop(){
        int res=0;
        try{
            lock.lock();//加鎖
            
            if(!dataAvailable){
                try{
                    cond.await();//等待
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            res=values[top];
            System.out.println("彈出數字"+res+"完成");
            top--;
            if(top<=0){
                dataAvailable=false;
                cond.signalAll();//喚醒線程
            }
            
        }finally{
            lock.unlock();//解鎖
        };
        return res;
    }
}

5.3 使用阻塞隊列(BlockingQueue)控制通訊

Java5提供了一個BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要用途不是用做容器,而是做爲線程同步的工具。BlockingQueue具備一個特徵,當生產者試圖向BlockingQueue中放入元素時,若是該隊列已經滿了,則線程被阻塞;當消費者線程試圖從BlockingQueue中取出元素時,若是該隊列爲空,則該線程被阻塞。程序的兩個線程經過交替想BlockingQueue中放入元素和取出元素,便可很好的控制線程通訊。

 

筆者認爲,該機制比上面提供的兩種線程異步通訊的機制更加的靈活、便捷,上面提供的兩種方式都須要同同樣東西,那就是須要顯示聲明臨界資源,以及指定阻塞和恢復執行的位置。而BlockingQueue對這一步進行了簡化,開發者只須要提供被同步資源的類型就能夠了,而無需關心具體的實現細節。在實際開發中,頗有可能遇到異步線程通訊的問題(而且沒有臨界資源),咱們能夠上面提供的兩種機制本身封裝能知足要求的異步線程通訊類,若是BlockingQueue能夠知足要求的話,那麼爲何不用別人已經封裝好的呢!

 

BlockingQueue提供以下兩個支持阻塞的方法:

put(E e):嘗試把e元素放入隊列中,若是該隊列中的元素已滿,那麼則阻塞該線程。

take():嘗試從BlockingQueue的頭部取出元素,若是該隊列的元素已空,則阻塞線程。

 

BlockingQueue繼承了Queue接口,固然也可使用Queue接口中的方法,大體有以下三類:

a.在隊列尾部插入元素。包括add(E e)、Offer(E e)和put(E e)方法,當隊列已滿時,這三個方法分別會拋出異常,返回false,阻塞隊列。

b.在隊列頭部刪除並返回刪除的元素。包括remove(),poll()和take()方法。當隊列已爲空時,這三個方法會分別拋出異常、返回false、阻塞隊列。

c.在隊列頭部取出但不刪除元素。包括element()和peek()方法,當隊列爲空時,這兩個方法分別拋出異常,返回false。

 

下面這張表映射它們之間的關係

  拋出異常 不一樣返回值 阻塞線程 指定超時時長
隊尾插入元素 add(e) offer(e) put(e) offer(e,time,unit)
隊頭插入元素 remove() poll() take() poll(time,unit)
獲取、不刪除元素 element() peek()

BlockingQueue與其實現關係的類圖:

ArrayBlockingQueue:基於數組實現的BlockingQueue隊列。

LinkedBlockingQueue:基於鏈表實現BlockingQueue隊列。

PriorityBlockingQueue:並非標準的阻塞隊列,該隊列使用remove()、poll()、take()等方法取元素時,並非取隊列中時間存在最長的元素,而是隊列中最小的元素。PriorityBlockingQueue<E>判斷元素大小能夠更具元素自己的大小排序(實現Comparable接口),也可使用Comparator進行定製排序。

SynchronousQueue:同步隊列。對該隊列的存取必須交替進行。

DelayQueue:它是一個特殊的BlockingQueue,底層依靠PriorityBlockingQueue實現。不過,DelayQueue要求集合元素都實現Delay接口(該接口裏有一個long getDelay()方法),DelayQueue根據集合元素的getDelay()方法的返回值排序。

 

下面是使用ArrayBlockingQueue實現異步線程通訊的一個案例:

public class BlackingQueueTest {

    public static void main(String[] args) {
        //建立一個容量爲1的BlockingQueue
        BlockingQueue<String> bq=new ArrayBlockingQueue<String>(1);
        
        //啓動三個線程
        new Producer(bq).start();
        new Producer(bq).start();
        new Consumer(bq).start();
    }
}
class Producer extends Thread{
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq){
        this.bq=bq;
    }
    public void run(){
        String[] strArr=new String[]{
                "JAVA",
                "STRUTS",
                "SPRING"
        };
        for(int i=0;i<99999;i++){
            System.out.println(getName()+"生產者準備生產集合元素!");
            try{
                Thread.sleep(200);
                //嘗試放入元素,若是元素已經滿,則線程會被阻塞
                bq.put(strArr[i%3]);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(getName()+"生產完成:"+bq);
        }
    }
}
class Consumer extends Thread{
    private BlockingQueue<String> bq;
    
    public Consumer(BlockingQueue<String> bq){
        this.bq=bq;
    }
    
    public void run(){
        while(true){
            System.out.println(getName()+"消費者準備消費集合元素");
            try{
                Thread.sleep(200);
                //嘗試取出元素,若是隊列已空,則線程會被阻塞
                bq.take();
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(getName()+"消費完成"+bq);
        }
    }
}

 

6. 線程組

java使用過ThreadGroup來表示線程組,它能夠對一組線程進行分類管理,java容許程序直接對線程組進行控制。對線程組的控制,至關於同時控制這批線程。用戶建立的全部的線程都屬於指定線程組,若是沒有顯示指定線程屬於哪一個線程組,則該線程屬於默認線程組。在默認狀況下,子線程和建立它的父線程處於同一個線程組內,例如A線程建立了B線程,而且沒有指定B線程的線程組,則B線程屬於A線程所在線程組。

一旦某個線程加入了指定線程組後,該線程將一直屬於該線程組,直到線程死亡,線程運行中不能改變它的所屬線程組。

Thread類提供了一個getThreadGroup()方法來返回該線程所屬線程組,getThreadGroup()方法的返回值是ThreadGroup對象,表示一個線程組。ThreadGroup類提供了以下兩個簡單的構造器來建立示例。
ThreadGroup(String name):以指定的線程組名字來建立新的線程組。
ThreadGroup(ThreadGroup parent,String name):以指定的名字和指定的父線程組建立一個新的線程組。

ThreadGroup類提供了以下幾個經常使用的方法來建立操做整個線程組裏的全部線程。
int activeCount():返回此線程組中活動線程的數目。
interrupt():中斷此線程中的全部線程。
isDaemon():判斷該線程組是不是後臺線程組。
setDaemon(boolean daemon):把該線程組設置爲後臺線程組(後臺線程組的一個特徵,當後臺線程組中的最後一個線程執行完畢或最後一個線程被銷燬後,後臺線程將自動銷燬)。
setMaxPriority(int pri):設置線程組的最高優先級。

7. 線程池

系統新啓動一個線程的成本是很高的,它涉及到與操做系統的交互。在這種狀況下,使用線程池能夠很好的提高性能,尤爲是線程中須要建立大量生存期短暫的線程時,更應該考慮使用線程池。

與數據庫池相似的是,線程池在啓動時即會建立大量空閒的線程,程序將一個Runnable對象或Callable對象傳給線程池,線程池就會啓動一個線程來執行他們的run()或call()方法,當run()或call()方法執行結束後,該線程並不會死亡,而是再次回到線程池後成爲空閒狀態。

從java5開始,java內建支持線程池。java5新增長了一個Executors工廠類來生產線程池,該工廠類有以下靜態方法:
newCachedThreadPool():建立一個具備緩存功能的線程池,系統根據須要建立線程,這些線程將會被緩存在線程池中。
ExecutorService newFixedThreadPool(int nThreads):建立一個可重用、具備固定線程數的線程池。
ExecutorService newSignleThreadExecutor():建立一個只有單線程的線程池,它至關於調用newFixedThreadPool()方法時傳入參數爲1。

ExecutorService newScheduledThreadPool(int corePoolSize):建立一個具備指定線程數的線程池,它能夠在指定延遲後執行線程任務。corePoolSize指池中所保存的線程數,即便線程空閒也被保存。
ExecutorService newSignleScheduledExecutor():建立只有一個線程的線程池,它能夠在指定延遲後執行線程任務。

使用線程池來執行線程任務的步驟以下:
a.調用Executors類的靜態工廠方法建立一個ExecutorService對象,該對象表明一個線程池。
b.建立Runnable實現類或Callable實現類的實例,做爲線程執行任務。
c.調用ExecutorService對象的submit()方法來提交Runnable或Callable的實例。
d.當不想提交任務時,就調用ExecutorService的shutdown()方法來關閉線程池,shutdown()方法也會將之前全部已提交的任務執行完畢。

栗子:

        ExecutorService pool= Executors.newFixedThreadPool(10);
        Runnable target=new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    System.out.println(Thread.currentThread().getName()+"的i值爲:"+i);
                }
            }
        };
        //向線程池中提交兩個線程
        pool.submit(target);
        pool.submit(target);
        //關閉線程池,執行池中已有的任務
        pool.shutdown();

8. 線程相關的類和方法

8.1 線程未處理的異常

java5開始,java增強了對線程異常的處理,若是線程執行過程當中拋出了一個未處理的異常,JVM在結束線程以前會自動檢查是否有對應的Thread.UncaughtExceptionHandler對象,若是找到該處理器對象,則調用該對象uncaughtException(Thread t,Throwable e)方法來處理該異常。

Thread.UncaughtExeceptionHandler是Thread類的一個靜態內部接口,該接口內只有一個方法:void uncaughtException(Thread t,Throwable e),該方法中的t表明出現的異常,而e表明該線程拋出的異常。
Thread類提供了一下兩個方法來設置異常處理器。
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):爲該線程類的全部線程實例設置默認的異常處理器。
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):爲指定的線程實例設置異常處理器。

注意:設置異常處理器只可以監聽到未處理的異常,而不能阻止它繼續向上拋出。

栗子:

class MyExHandler implements Thread.UncaughtExceptionHandler{

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t+" 線程出現了異常:"+e);
    }
}
public class ExHandler {
    public static void main(String[] args) {
        Thread.currentThread().setDefaultUncaughtExceptionHandler(new MyExHandler());
        
        int a =5/0;
        
        System.out.println("程序正常結束");
    }
}

打印爲:

Thread[main,5,main] 線程出現了異常:java.lang.ArithmeticException: / by zero

從打印看出,設置UncaughtExceptionHandler不會阻止異常繼續往上拋出,若要阻止的話可使用try catch來完成。

8.2 包裝線程不安全的集合

java集合中的ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是線程不安全的,也就是說,多個線程併發訪問這些集合中的元素時,就有可能破壞數據的完整性。
針對這個問題,java提供提供了Collections類,使用Collections類能夠把這些集合包裝成線程安全的。
例如:
//將一個普通的HashMap包裝爲線程安全的HashMap對象

HashMap m=Collections.synchronizedMap(new HashMap());

關於Collections的更多使用,能夠詳見java API。

相關文章
相關標籤/搜索