Java多線程詳解

轉:

http://www.cnblogs.com/lwbqqyumidi/p/3804883.html

http://www.cnblogs.com/snow-flower/p/6114765.html


  線程對象是可以產生線程的對象。比如在Java平臺中Thread對象,Runnable對象。線程,是指正在執行的一個指點令序列。在java平臺上是指從一個線程對象的start()開始,運行run方法體中的那一段相對獨立的過程。相比於多進程,多線程的優勢有:

    (1)進程之間不能共享數據,線程可以;

    (2)系統創建進程需要爲該進程重新分配系統資源,故創建線程代價比較小;

    (3)Java語言內置了多線程功能支持,簡化了java多線程編程。


一.線程的生命週期及五種基本狀態

關於Java中線程的生命週期,首先看一下下面這張較爲經典的圖:



上圖中基本上囊括了Java中多線程各重要知識點。掌握了上圖中的各知識點,Java中的多線程也就基本上掌握了。主要包括:

Java線程具有五中基本狀態

Java中線程的創建常見有如三種基本形式

1.繼承Thread類,重寫該類的run()方法。

中基本狀態

新建狀態(New)當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable)當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

運行狀態(Running)當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就     緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

阻塞狀態(Blocked)處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

死亡狀態(Dead)線程執行完了或者因異常退出了run()方法,該線程結束生命週期。



二. Java多線程的創建及啓動

Java中線程的創建常見有如三種基本形式

1.繼承Thread類,重寫該類的run()方法。

class MyThread extends Thread {
    
    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread myThread1 = new MyThread();     // 創建一個新的線程  myThread1  此線程進入新建狀態
                Thread myThread2 = new MyThread();     // 創建一個新的線程 myThread2 此線程進入新建狀態
                myThread1.start();                     // 調用start()方法使得線程進入就緒狀態
                myThread2.start();                     // 調用start()方法使得線程進入就緒狀態
            }
        }
    }
}

如上所示,繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之爲線程執行體。當創建此線程類對象時一個新的線程得以創建,並進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不一定會馬上得以執行,這取決於CPU調度時機。


2.實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象。

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable(); // 創建一個Runnable實現類的對象
                Thread thread1 = new Thread(myRunnable); // 將myRunnable作爲Thread target創建新的線程
                Thread thread2 = new Thread(myRunnable);
                thread1.start(); // 調用start()方法使得線程進入就緒狀態
                thread2.start();
            }
        }
    }
}

相信以上兩種創建新線程的方式大家都很熟悉了,那麼Thread和Runnable之間到底是什麼關係呢?我們首先來看一下下面這個例子。

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class MyThread extends Thread {

    private int i = 0;
    
    public MyThread(Runnable runnable){
        super(runnable);
    }

    @Override
    public void run() {
        System.out.println("in MyThread run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

同樣的,與實現Runnable接口創建線程方式相似,不同的地方在於

Thread thread = new MyThread(myRunnable);

那麼這種方式可以順利創建出一個新的線程麼?答案是肯定的。至於此時的線程執行體到底是MyRunnable接口中的run()方法還是MyThread類中的run()方法呢?通過輸出我們知道線程執行體是MyThread類中的run()方法。其實原因很簡單,因爲Thread類本身也是實現了Runnable接口,而run()方法最先是在Runnable接口中定義的方法。

public interface Runnable {
   public abstract void run();
     
}
我們看一下Thread類中對Runnable接口中run()方法的實現:

 @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

也就是說,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable接口並重寫了run()方法的類中的run()方法。但是上述給到的列子中,由於多態的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了運行時類型即MyThread類中的run()方法。


3.使用Callable和Future接口創建線程。具體是創建Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作爲Thread對象的target來創建線程。

 看着好像有點複雜,直接來看一個例子就清晰了。

public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 創建MyCallable對象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask對象作爲Thread對象的target創建新的線程
                thread.start();                      //線程進入到就緒狀態
            }
        }

        System.out.println("主線程for循環執行完畢..");
        
        try {
            int sum = ft.get();            //取得新創建的新線程中的call()方法返回的結果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 與run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

首先,我們發現,在實現Callable接口中,此時不再是run()方法了,而是call()方法,此call()方法作爲線程執行體,同時還具有返回值!在創建新的線程時,是通過FutureTask來包裝MyCallable對象,同時作爲了Thread對象的target。那麼看下FutureTask類的定義:

public class FutureTask<V> implements RunnableFuture<V> {
    
     //....
    
 }
public interface RunnableFuture<V> extends Runnable, Future<V> { 
     void run();
}

於是,我們發現FutureTask類實際上是同時實現了Runnable和Future接口,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作爲Thread對象的target,而Future特性,使得其可以取得新創建線程中的call()方法的返回值。


執行下此程序,我們發現sum = 4950永遠都是最後輸出的。而“主線程for循環執行完畢..”則很可能是在子線程循環中間輸出。由CPU的線程調度機制,我們知道,“主線程for循環執行完畢..”的輸出時機是沒有任何問題的,那麼爲什麼sum =4950會永遠最後輸出呢?


原因在於通過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。


上述主要講解了三種常見的線程創建方式,對於線程的啓動而言,都是調用線程對象的start()方法,需要特別注意的是:不能對同一線程對象兩次調用start()方法


三、線程管理

Java提供了一些便捷的方法用於會線程狀態的控制。具體如下:

1、線程睡眠——sleep
      如果我們需要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過調用Thread的sleep方法。

注:
   (1)sleep是靜態方法,最好不要用Thread的實例對象調用它,因爲它睡眠的始終是當前正在運行的線程,而不是調用它的線程對象,它只對正在運行狀態的線程對象有效。如下面的例子:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//這裏sleep的就是main線程,而非myThread線程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
}


(2)Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程序的執行效率。但是不管程序員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能做到精準控制。因爲使用sleep方法之後,線程是進入阻塞狀態的,只有當睡眠的時間結束,纔會重新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,我們不可能精準的去幹涉它,所以如果調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。


2、線程讓步——yield

      yield()方法和sleep()方法有點相似,它也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的線程暫停,讓出cpu資源給其他的線程。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前線程暫停一下,重新進入就緒的線程池中,讓系統的線程調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個線程調用yield()方法之後,線程調度器又將其調度出來重新進入到運行狀態執行。


實際上,當某個線程調用了yield()方法暫停之後,優先級與當前線程相同,或者優先級比當前線程更高的就緒狀態的線程更有可能獲得執行的機會,當然,只是有可能,因爲我們不可能精確的干涉cpu調度線程。用法如下:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低級", 1).start();  
        new MyThread("中級", 5).start();  
        new MyThread("高級", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 設置線程的名稱  
        this.setPriority(pro);// 設置優先級  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "線程第" + i + "次執行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
}

:關於sleep()方法和yield()方的區別如下:

①、sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,纔會轉入就緒狀態。而yield方法調用後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態。
②、sleep方法聲明拋出了InterruptedException,所以調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務異常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制併發線程的執行。

3、線程合併——join
線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程執行,應用場景是當一個線程必須等待另一個線程執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。
從上面的方法的列表可以看到,它有3個重載的方法:

void join()      
     當前線程等該加入該線程後面,等待該線程終止。    
void join(long millis)  
     當前線程等待該線程終止的時間最長爲 millis 毫秒。 如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度  
void join(long millis,int nanos)   
     等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度 


public class TestJoin {
 
  public static void main(String[] args) {
   
    MyThread2 t1 = new MyThread2("TestJoin");
    t1.start();
    try {
      t1.join();  //join()合併線程,子線程運行完之後,主線程纔開始執行
     }catch (InterruptedException e) {  }
      
     for(int i=0 ; i <10; i++)
              System.out.println("I am Main Thread");
   }
 }
 
 class MyThread2 extends Thread {
  
    MyThread2(String s) {
     super(s);
     }
     
  public void run() {
    for(int i = 1; i <= 10; i++) {
     System.out.println("I am "+getName());
     try {
      sleep(1000); //暫停,每一秒輸出一次
      }catch (InterruptedException e) {
      return;
     }
     }
   }
  }



4、設置線程的優先級


     每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的也並非沒機會執行。


每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級。


注:Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參數是一個整數,範圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5
public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("高級", 10).start();  
            new MyThread("低級", 1).start();  
        }  
    }  
      
    class MyThread extends Thread {  
        public MyThread(String name,int pro) {  
            super(name);//設置線程的名稱  
            setPriority(pro);//設置線程的優先級  
        }  
        @Override  
        public void run() {  
            for (int i = 0; i < 100; i++) {  
                System.out.println(this.getName() + "線程第" + i + "次執行!");  
            }  
        }  
    }

注:雖然Java提供了10個優先級別,但這些優先級別需要操作系統的支持。不同的操作系統的優先級並不相同,而且也不能很好的和Java的10個優先級別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先級,這樣才能保證程序最好的可移植性。



5、後臺(守護)線程
http://blog.csdn.net/u014695188/article/details/62881023

     守護線程使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含着很多後臺線程,監控連接個數、超時時間、狀態等等。調用線程對象的方法setDaemon(true),則可以將其設置爲守護線程。守護線程的用途爲:
     • 守護線程通常用於執行一些後臺作業,例如在你的應用程序運行時播放背景音樂,在文字編輯器裏做自動語法檢查、自動保存等功能。


     • Java的垃圾回收也是一個守護線程。守護線的好處就是你不需要關心它的結束問題。例如你在你的應用程序運行的時候希望播放背景音樂,如果將這個播放背景音樂的線程設定爲非守護線程,那麼在用戶請求退出的時候,不僅要退出主線程,還要通知播放背景音樂的線程退出;如果設定爲守護線程則不需要了。


setDaemon方法的詳細說明:

public final void setDaemon(boolean on)        將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。    
         該方法必須在啓動線程前調用。 該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。   
  參數:
     on - 如果爲 true,則將該線程標記爲守護線程。    
  拋出:    
    IllegalThreadStateException - 如果該線程處於活動狀態。    
    SecurityException - 如果當前線程無法修改該線程。
注:JRE判斷程序是否執行結束的標準是所有的前臺執線程行完畢了,而不管後臺線程的狀態,因此,在使用後臺線程時候一定要注意這個問題。


6、正確結束線程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運行的方法已經被廢棄了,使用它們是極端不安全的!想要安全有效的結束一個線程,可以使用下面的方法:

    • 正常執行完run方法,然後結束掉;

    • 控制循環條件和判斷條件的標識符來結束掉線程。

class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}

四、線程同步

     java允許多線程併發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生衝突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。

1、同步方法     

      即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。

public synchronized void save(){}

注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類


 2、同步代碼塊
    

     即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步

public class Bank {  
     
        private int count =0;//賬戶餘額  
     
        //存錢  
        public   void addMoney(int money){  
     
            synchronized (this) {  
                count +=money;  
            }  
            System.out.println(System.currentTimeMillis()+"存進:"+money);  
        }  
     
        //取錢  
        public   void subMoney(int money){  
     
            synchronized (this) {  
                if(count-money < 0){  
                    System.out.println("餘額不足");  
                    return;  
                }  
                count -=money;  
            }  
            System.out.println(+System.currentTimeMillis()+"取出:"+money);  
        }  
     
        //查詢  
        public void lookMoney(){  
            System.out.println("賬戶餘額:"+count);  
        } 
    }

注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。


 3、使用特殊域變量(volatile)實現線程同步    
 


   • volatile關鍵字爲域變量的訪問提供了一種免鎖機制;

   • 使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新;

   • 因此每次使用該域就要重新計算,而不是使用寄存器中的值;

   • volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。

public class SynchronizedThread {
 
        class Bank {
 
            private volatile int account = 100;
 
            public int getAccount() {
                return account;
            }
 
            /**
             * 用同步方法實現
             * 
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }
 
            /**
             * 用同步代碼塊實現
             * 
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }
 
        class NewThread implements Runnable {
            private Bank bank;
 
            public NewThread(Bank bank) {
                this.bank = bank;
            }
 
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "賬戶餘額爲:" +bank.getAccount());
                }
            }
 
        }
 
        /**
         * 建立線程,調用內部類
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("線程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("線程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }
 
        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }

注:多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。用final域,有鎖保護的域和volatile域可以避免非同步的問題。


4、使用重入鎖(Lock)實現線程同步


      在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力。ReenreantLock類的常用方法有:

 ReentrantLock() : 創建一個ReentrantLock實例         
 lock() : 獲得鎖        
 unlock() : 釋放鎖

注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用

//只給出要修改的代碼,其餘代碼與上同
        class Bank {
            
            private int account = 100;
            //需要聲明這個鎖
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //這裏不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
                
            }
        }

五、線程通信



1、藉助於Object類的wait()、notify()和notifyAll()實現通信


     線程執行wait()後,就放棄了運行資格,處於凍結狀態;


     線程運行時,內存中會建立一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結的線程。
      notifyall(), 喚醒線程池中所有線程。
注: (1) wait(), notify(),notifyall()都用在同步裏面,因爲這3個函數是對持有鎖的線程進行操作,而只有同步纔有鎖,所以要使用在同步中;
       (2) wait(),notify(),notifyall(),  在使用時必須標識它們所操作的線程持有的鎖,因爲等待和喚醒必須是同一鎖下的線程;而鎖可以是任意對象,所以這3個方法都是Object類中的方法。


單個消費者生產者例子如下:

class Resource{  //生產者和消費者都要操作的資源  
    private String name;  
    private int count=1;  
    private boolean flag=false;  
    public synchronized void set(String name){  
        if(flag)  
            try{wait();}catch(Exception e){}  
        this.name=name+"---"+count++;  
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
        flag=true;  
        this.notify();  
    }  
    public synchronized void out(){  
        if(!flag)  
            try{wait();}catch(Exception e){}  
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);  
        flag=false;  
        this.notify();  
    }  
}  
class Producer implements Runnable{  
    private Resource res;  
    Producer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.set("商品");  
        }  
    }  
}  
class Consumer implements Runnable{  
    private Resource res;  
    Consumer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.out();  
        }  
    }  
}  
public class ProducerConsumerDemo{  
    public static void main(String[] args){  
        Resource r=new Resource();  
        Producer pro=new Producer(r);  
        Consumer con=new Consumer(r);  
        Thread t1=new Thread(pro);  
        Thread t2=new Thread(con);  
        t1.start();  
        t2.start();  
    }  
}//運行結果正常,生產者生產一個商品,緊接着消費者消費一個商品。

但是如果有多個生產者和多個消費者,上面的代碼是有問題,比如2個生產者,2個消費者,運行結果就可能出現生產的1個商品生產了一次而被消費了2次,或者連續生產2個商品而只有1個被消費,這是因爲此時共有4個線程在操作Resource對象r,  而notify()喚醒的是線程池中第1個wait()的線程,所以生產者執行notify()時,喚醒的線程有可能是另1個生產者線程,這個生產者線程從wait()中醒來後不會再判斷flag,而是直接向下運行打印出一個新的商品,這樣就出現了連續生產2個商品。
爲了避免這種情況,修改代碼如下
class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        public synchronized void set(String name){  
            while(flag) /*原先是if,現在改成while,這樣生產者線程從凍結狀態醒來時,還會再判斷flag.*/  
                try{wait();}catch(Exception e){}  
            this.name=name+"---"+count++;  
            System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
            flag=true;  
            this.notifyAll();/*原先是notity(), 現在改成notifyAll(),這樣生產者線程生產完一個商品後可以將等待中的消費者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/  
        }  
        public synchronized void out(){  
            while(!flag) /*原先是if,現在改成while,這樣消費者線程從凍結狀態醒來時,還會再判斷flag.*/  
                try{wait();}catch(Exception e){}  
            System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);  
            flag=false;  
            this.notifyAll(); /*原先是notity(), 現在改成notifyAll(),這樣消費者線程消費完一個商品後可以將等待中的生產者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/  
        }  
    }  
    public class ProducerConsumerDemo{  
        public static void main(String[] args){  
            Resource r=new Resource();  
            Producer pro=new Producer(r);  
            Consumer con=new Consumer(r);  
            Thread t1=new Thread(pro);  
            Thread t2=new Thread(con);  
            Thread t3=new Thread(pro);  
            Thread t4=new Thread(con);  
            t1.start();  
            t2.start();  
            t3.start();  
            t4.start();  
        }  
    }

2、使用Condition控制線程通信



      jdk1.5中,提供了多線程的升級解決方案爲:


     (1)將同步synchronized替換爲顯式的Lock操作;

     (2)將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象可以通過Lock鎖對象獲取;

     (3)一個Lock對象上可以綁定多個Condition對象,這樣實現了本方線程只喚醒對方線程,而jdk1.5之前,一個同步只能有一個鎖,不同的同步只能用鎖來區分,且鎖嵌套時容易死鎖。


class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        private Lock lock = new ReentrantLock();/*Lock是一個接口,ReentrantLock是該接口的一個直接子類。*/  
        private Condition condition_pro=lock.newCondition(); /*創建代表生產者方面的Condition對象*/  
        private Condition condition_con=lock.newCondition(); /*使用同一個鎖,創建代表消費者方面的Condition對象*/  
          
        public void set(String name){  
            lock.lock();//鎖住此語句與lock.unlock()之間的代碼  
            try{  
                while(flag)  
                    condition_pro.await(); //生產者線程在conndition_pro對象上等待  
                this.name=name+"---"+count++;  
                System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
                flag=true;  
                 condition_con.signalAll();  
            }  
            finally{  
                lock.unlock(); //unlock()要放在finally塊中。  
            }  
        }  
        public void out(){  
            lock.lock(); //鎖住此語句與lock.unlock()之間的代碼  
            try{  
                while(!flag)  
                    condition_con.await(); //消費者線程在conndition_con對象上等待  
            System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);  
            flag=false;  
            condition_pro.signqlAll(); /*喚醒所有在condition_pro對象下等待的線程,也就是喚醒所有生產者線程*/  
            }  
            finally{  
                lock.unlock();  
            }  
        }  
    }


3、使用阻塞隊列(BlockingQueue)控制線程通信



       BlockingQueue是一個接口,也是Queue的子接口。BlockingQueue具有一個特徵:當生產者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則線程被阻塞;但消費者線程試圖從BlockingQueue中取出元素時,如果隊列已空,則該線程阻塞。程序的兩個線程通過交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。


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


  (1)put(E e):嘗試把Eu元素放如BlockingQueue中,如果該隊列的元素已滿,則阻塞該線程。


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


BlockingQueue繼承了Queue接口,當然也可以使用Queue接口中的方法,這些方法歸納起來可以分爲如下三組:


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


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


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


BlockingQueue接口包含如下5個實現類:
ArrayBlockingQueue :基於數組實現的BlockingQueue隊列。

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

PriorityBlockingQueue:它並不是保準的阻塞隊列,該隊列調用remove()、poll()、take()等方法提取出元素時,並不是取出隊列中存在時間最長的元素,而是隊列中最小的元素。
                       它判斷元素的大小即可根據元素(實現Comparable接口)的本身大小來自然排序,也可使用Comparator進行定製排序。

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

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

copy的一個示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest{
    public static void main(String[] args)throws Exception{
        //創建一個容量爲1的BlockingQueue
        
        BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
        //啓動3個生產者線程
        new Producer(b).start();
        new Producer(b).start();
        new Producer(b).start();
        //啓動一個消費者線程
        new Consumer(b).start();
        
    }
}
class Producer extends Thread{
    private BlockingQueue<String> b;
    
    public Producer(BlockingQueue<String> b){
        this.b=b;
        
    }
    public synchronized void run(){
        String [] str=new String[]{
            "java",
            "struts",
            "Spring"
        };
        for(int i=0;i<9999999;i++){
            System.out.println(getName()+"生產者準備生產集合元素!");
            try{
            
                b.put(str[i%3]);
                sleep(1000);
                //嘗試放入元素,如果隊列已滿,則線程被阻塞
                
            }catch(Exception e){System.out.println(e);}
            System.out.println(getName()+"生產完成:"+b);
        }
        
    }
}
class Consumer extends Thread{
    private BlockingQueue<String> b;
    public Consumer(BlockingQueue<String> b){
        this.b=b;
    }
    public  synchronized  void run(){
    
        while(true){
            System.out.println(getName()+"消費者準備消費集合元素!");
            try{
                sleep(1000);
                //嘗試取出元素,如果隊列已空,則線程被阻塞
                b.take();
            }catch(Exception e){System.out.println(e);}
            System.out.println(getName()+"消費完:"+b);
        }
    
    }
}




發佈了0 篇原創文章 · 獲贊 126 · 訪問量 92萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章