Java多執行緒程式設計——進階篇二

九天高遠發表於2013-09-12

一、執行緒的互動

a、執行緒互動的基礎知識

執行緒互動知識點需要從java.lang.Object的類的三個方法來學習:
 
 void notify() 
          喚醒在此物件監視器上等待的單個執行緒(notify()方法呼叫的時候,鎖並沒有被釋放)。 
 void notifyAll() 
          喚醒在此物件監視器上等待的所有執行緒。 
 void wait() 
          導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法(wait()方法釋放當前鎖)
 
當然,wait()還有另外兩個過載方法:
 void wait(long timeout) 
          導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。 
 void wait(long timeout, int nanos) 
          導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
 
以上這些方法是幫助執行緒傳遞執行緒關心的時間狀態。
關於wait/notify,要記住的關鍵點是:
  1. 必須從同步環境內呼叫wait()、notify()、notifyAll()方法。
  2. 執行緒不能呼叫物件上的wait或notify的方法,除非它擁有那個物件的鎖。
  3. wait()、notify()、notifyAll()都是Object的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號(通知)。執行緒透過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。
 
下面看個例子就明白了:
/** 
* 計算輸出其他執行緒鎖計算的資料 

*  
*/
 

public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //啟動計算執行緒 
        b.start(); 
        //主執行緒擁有b物件上的鎖。執行緒為了呼叫wait()或notify()方法,該執行緒必須是那個物件鎖的擁有者 
        synchronized (b) { 
            try { 
                System.out.println("等待物件b完成計算..."); 
                //當前主執行緒等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b物件計算的總和是:" + b.total); 
        } 
    } 
}
 
/** 
* 計算1+2+3 ... +100的和 
* 
*  
*/ 
public class ThreadB extends Thread { 
    int total; 

    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
            //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中線主執行緒被喚醒 
            notify(); 
        } 
    } 
}
 
執行結果:
等待物件b完成計算...
b物件計算的總和是:5050

千萬注意:

當在物件上呼叫wait()方法時,執行該程式碼的執行緒(主執行緒)立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味著這時執行緒會放棄其鎖。如果執行緒仍然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,只要呼叫notify()並不意味著這時該鎖變得可用。

b、多個執行緒在等待一個物件鎖時候使用notifyAll()

在多數情況下,最好通知等待某個物件的所有執行緒。如果這樣做,可以在物件上使用notifyAll()讓所有在此物件上等待的執行緒衝出等待區,返回到可執行狀態。
 
下面給個例子:
/** 
* 計算執行緒 


*/
 

public class Calculator extends Thread { 
        int total; 

        public void run() { 
                synchronized (this) { 
                        for (int i = 0; i < 101; i++) { 
                                total += i; 
                        } 
                } 
                //通知在此物件上等待的所有執行緒 
                notifyAll(); 
        } 
}
 
/** 
* 獲取計算結果並輸出 

*  
*/
 

public class ReaderResult extends Thread { 
        Calculator c; 

        public ReaderResult(Calculator c) { 
                this.c = c; 
        } 

        public void run() { 
                synchronized (c) { 
                        try { 
                                System.out.println(Thread.currentThread() + "等待計算結果。。。"); 
                                c.wait(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                        System.out.println(Thread.currentThread() + "計算結果為:" + c.total); 
                } 
        } 

        public static void main(String[] args) { 
                Calculator calculator = new Calculator(); 

                //啟動三個執行緒,分別獲取計算結果 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                //啟動計算執行緒 
                calculator.start(); 
        } 
}
 
執行結果:
Thread[Thread-1,5,main]等待計算結果... 
Thread[Thread-2,5,main]等待計算結果... 
Thread[Thread-3,5,main]等待計算結果... 
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner 
  at java.lang.Object.notifyAll(Native Method) 
  at threadtest.Calculator.run(Calculator.java:18) 
Thread[Thread-1,5,main]計算結果為:5050 
Thread[Thread-2,5,main]計算結果為:5050 
Thread[Thread-3,5,main]計算結果為:5050 
執行結果表明,程式中有異常,並且多次執行結果可能有多種輸出結果。這就是說明,這個多執行緒的互動程式還存在問題。究竟是出了什麼問題,需要深入的分析和思考,下面將做具體分析。
實際上,上面這個程式碼中,我們期望的是讀取結果的執行緒在計算執行緒呼叫notifyAll()之前等待即可。 但是,如果計算執行緒先執行,並在讀取結果執行緒等待之前呼叫了notify()方法,那麼又會發生什麼呢?這種情況是可能發生的。因為無法保證執行緒的不同部分將按照什麼順序來執行。幸運的是當讀取執行緒執行時,它只能馬上進入等待狀態----它沒有做任何事情來檢查等待的事件是否已經發生。  ----因此,如果計算執行緒已經呼叫了notifyAll()方法,那麼它就不會再次呼叫notifyAll(),----並且等待的讀取執行緒將永遠保持等待。這當然是開發者所不願意看到的問題。
 
因此,當等待的事件發生時,需要能夠檢查notifyAll()通知事件是否已經發生。
問題是如何檢查通知事件是否發生?

二、執行緒的排程

a、休眠 

Java執行緒排程是Java多執行緒的核心,只有良好的排程,才能充分發揮系統的效能,提高程式的執行效率。
 這裡要明確的一點,不管程式設計師怎麼編寫排程,只能最大限度的影響執行緒執行的次序,而不能做到精準控制。
 執行緒休眠的目的是使執行緒讓出CPU的最簡單的做法之一,執行緒休眠時候,會將CPU資源交給其他執行緒,以便能輪換執行,當休眠一定時間後,執行緒會甦醒,進入準備狀態等待執行。
 執行緒休眠的方法是Thread.sleep(long millis) 和Thread.sleep(long millis, int nanos) ,均為靜態方法,那呼叫sleep休眠的哪個執行緒呢?簡單說,哪個執行緒呼叫sleep,就休眠哪個執行緒。
/** 
* Java執行緒:執行緒的排程-休眠 
* 
**/ 
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.start(); 
                t2.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("執行緒1第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("執行緒2第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}

執行結果:

執行緒2第0次執行! 
執行緒1第0次執行! 
執行緒1第1次執行! 
執行緒2第1次執行! 
執行緒1第2次執行! 
執行緒2第2次執行! 

從上面的執行結果可以看出,無法精準保證執行緒的執行次序。

b、優先順序

與執行緒休眠類似,執行緒的優先順序仍然無法保障執行緒的執行次序。只不過,優先順序高的執行緒獲取CPU資源的機率較大,優先順序低的並非沒機會執行。
執行緒的優先順序用1-10之間的整數表示,數值越大優先順序越高,預設的優先順序為5。
在一個執行緒中開啟另外一個新執行緒,則新開執行緒稱為該執行緒的子執行緒,子執行緒初始優先順序與父執行緒相同
 
/** 
* Java執行緒:執行緒的排程-優先順序 
* 
* */ 
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.setPriority(10); 
                t2.setPriority(1); 

                t2.start(); 
                t1.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("執行緒1第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("執行緒2第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}

執行結果:

執行緒1第0次執行! 
執行緒2第0次執行! 
執行緒2第1次執行! 
執行緒1第1次執行! 
執行緒2第2次執行! 
執行緒1第2次執行! 
執行緒1第3次執行! 
執行緒2第3次執行! 
執行緒2第4次執行! 
執行緒1第4次執行! 
執行緒1第5次執行! 
執行緒2第5次執行! 
執行緒1第6次執行! 
執行緒2第6次執行! 
執行緒1第7次執行! 
執行緒2第7次執行! 
執行緒1第8次執行! 
執行緒2第8次執行! 
執行緒1第9次執行! 
執行緒2第9次執行! 

由上面的執行結果可以看出,雖然執行緒1的優先順序較高,在真正這行的時候並沒有絕對的優勢,只是其獲得執行的機率較大。

c、讓步

執行緒的讓步含義就是使當前執行著執行緒讓出CPU資源,但是讓給誰不知道,僅僅是讓出,執行緒狀態回到可執行狀態。
執行緒的讓步使用Thread.yield()方法,yield() 為靜態方法,功能是暫停當前正在執行的執行緒物件,並執行其他執行緒。
/** 
* Java執行緒:執行緒的排程-讓步 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 

                t2.start(); 
                t1.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("執行緒1第" + i + "次執行!"); 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("執行緒2第" + i + "次執行!"); 
                        Thread.yield(); 
                } 
        } 
}

執行結果:

執行緒2第0次執行! 
執行緒2第1次執行! 
執行緒2第2次執行! 
執行緒2第3次執行! 
執行緒1第0次執行! 
執行緒1第1次執行! 
執行緒1第2次執行! 
執行緒1第3次執行! 
執行緒1第4次執行! 
執行緒1第5次執行! 
執行緒1第6次執行! 
執行緒1第7次執行! 
執行緒1第8次執行! 
執行緒1第9次執行! 
執行緒2第4次執行! 
執行緒2第5次執行! 
執行緒2第6次執行! 
執行緒2第7次執行! 
執行緒2第8次執行! 
執行緒2第9次執行! 

從上面的執行結果可以看出,讓步的執行緒最後執行完畢。

 

d、合併

執行緒的合併的含義就是將幾個並行執行緒的執行緒合併為一個單執行緒執行,應用場景是當一個執行緒必須等待另一個執行緒執行完畢才能執行時可以使用join方法。

join為非靜態方法,定義如下:

void join()    
    等待該執行緒終止。    
void join(long millis)    
    等待該執行緒終止的時間最長為 millis 毫秒。    
void join(long millis, int nanos)    
    等待該執行緒終止的時間最長為 millis 毫秒 + nanos 納秒。

 

/** 
* Java執行緒:執行緒的排程-合併 
* 
* */ 
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                t1.start(); 

                for (int i = 0; i < 20; i++) { 
                        System.out.println("主執行緒第" + i + "次執行!"); 
                        if (i > 2) try { 
                                //t1執行緒合併到主執行緒中,主執行緒停止執行過程,轉而執行t1執行緒,直到t1執行完畢後繼續。 
                                t1.join(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("執行緒1第" + i + "次執行!"); 
                } 
        } 
}

執行結果:

主執行緒第0次執行! 
主執行緒第1次執行! 
主執行緒第2次執行! 
執行緒1第0次執行! 
主執行緒第3次執行! 
執行緒1第1次執行! 
執行緒1第2次執行! 
執行緒1第3次執行! 
執行緒1第4次執行! 
執行緒1第5次執行! 
執行緒1第6次執行! 
執行緒1第7次執行! 
執行緒1第8次執行! 
執行緒1第9次執行! 
主執行緒第4次執行! 
主執行緒第5次執行! 
主執行緒第6次執行! 
主執行緒第7次執行! 
主執行緒第8次執行! 
主執行緒第9次執行! 
主執行緒第10次執行! 
主執行緒第11次執行! 
主執行緒第12次執行! 
主執行緒第13次執行! 
主執行緒第14次執行! 
主執行緒第15次執行! 
主執行緒第16次執行! 
主執行緒第17次執行! 
主執行緒第18次執行! 
主執行緒第19次執行!

從上面的執行結果中可以看出,使用join方法新增到主執行緒後,主執行緒停止執行,等待子執行緒執行結束後,主執行緒繼續執行至結束。

e、守護執行緒

守護執行緒與普通執行緒寫法上基本麼啥區別,呼叫執行緒物件的方法setDaemon(true),則可以將其設定為守護執行緒。
守護執行緒使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用時候,使用的資料庫連線池,連線池本身也包含著很多後臺執行緒,監控連線個數、超時時間、狀態等等。
setDaemon方法的詳細說明:
 
public final void setDaemon(boolean on)將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。    
  該方法必須在啟動執行緒前呼叫。    

  該方法首先呼叫該執行緒的 checkAccess 方法,且不帶任何引數。這可能丟擲 SecurityException(在當前執行緒中)。    


  引數: 
    on - 如果為 true,則將該執行緒標記為守護執行緒。    
  丟擲:    
    IllegalThreadStateException - 如果該執行緒處於活動狀態。    
    SecurityException - 如果當前執行緒無法修改該執行緒。 
  另請參見: 
    isDaemon(), checkAccess()
/** 
* Java執行緒:執行緒的排程-守護執行緒 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyCommon(); 
                Thread t2 = new Thread(new MyDaemon()); 
                t2.setDaemon(true);        //設定為守護執行緒 

                t2.start(); 
                t1.start(); 
        } 
} 

class MyCommon extends Thread { 
        public void run() { 
                for (int i = 0; i < 5; i++) { 
                        System.out.println("執行緒1第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(7); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyDaemon implements Runnable { 
        public void run() { 
                for (long i = 0; i < 9999999L; i++) { 
                        System.out.println("後臺執行緒第" + i + "次執行!"); 
                        try { 
                                Thread.sleep(7); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}

執行結果:

後臺執行緒第0次執行! 
執行緒1第0次執行! 
執行緒1第1次執行! 
後臺執行緒第1次執行! 
後臺執行緒第2次執行! 
執行緒1第2次執行! 
執行緒1第3次執行! 
後臺執行緒第3次執行! 
執行緒1第4次執行! 
後臺執行緒第4次執行! 
後臺執行緒第5次執行! 
後臺執行緒第6次執行! 
後臺執行緒第7次執行! 
從上面的執行結果可以看出:
前臺執行緒是保證執行完畢的,後臺執行緒還沒有執行完畢就退出了。
 
實際上:JRE判斷程式是否執行結束的標準是所有的前臺執執行緒行完畢了,而不管後臺執行緒的狀態,因此,在使用後臺執行緒時候一定要注意這個問題。
 

f、同步方法

執行緒的同步是保證多執行緒安全訪問競爭資源的一種手段。
執行緒的同步是Java多執行緒程式設計的難點,往往開發者搞不清楚什麼是競爭資源、什麼時候需要考慮同步,怎麼同步等等問題,當然,這些問題沒有很明確的答案,但有些原則問題需要考慮,是否有競爭資源被同時改動的問題?
對於同步,在具體的Java程式碼中需要完成一下兩個操作:
  • 把競爭訪問的資源標識為private;
  • 同步哪些修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。當然這不是唯一控制併發安全的途徑。
synchronized關鍵字使用說明
synchronized只能標記非抽象的方法,不能標識成員變數。
  為了演示同步方法的使用,構建了一個信用卡賬戶,起初信用額為100,然後模擬透支、存款等多個操作。顯然銀行賬戶Account物件是個競爭資源,而多個併發操作的是賬戶方法takeMoney(int money)和putMoney(int money),當然應該在此方法上加上同步,並將賬戶的餘額設為私有變數,禁止直接訪問

 

package MultiThread;

/**
 * 此處模擬的為信用卡賬戶
 * */
public class PutTakeMoney {

     
    /**
     * @author donghe
     *
     */
    static class Account{
           private String Id;
           private int remain;
           public Account(){
               
           }
           public Account(String id){
               this.setId(id);
               this.remain=100;
           }
           public Account(String id, int initRemain ){
               this.setId(id);
               this.remain=initRemain;
           }
           
           public int getRemain(){
               return this.remain;
           }
           public void setRemain(int r){
               this.remain=r;
           }
           
            /**
             * 存款的方法
             *
             */
         public synchronized void putMoney(int money){
             try{
             Thread.sleep(10L);
             this.remain+=money;
             System.out.println(Thread.currentThread().getName()+"存款後,餘額為:"+this.remain);
             Thread.sleep(10L);
             }catch(InterruptedException ine){
                 ine.printStackTrace();
         }
         }
         
         /**
             * 取款的方法
             *
             */
         public synchronized void takeMoney(int money){
             try{
             Thread.sleep(10L);
             this.remain-=money;
             System.out.println(Thread.currentThread().getName()+"取款後,餘額為:"+this.remain);
             Thread.sleep(10L);
             }catch(InterruptedException ine){
                 ine.printStackTrace();
         }
         }
        public String getId() {
            return Id;
        }
        public void setId(String id) {
            Id = id;
        }
    }     
         
    static class putThread implements Runnable{
        private Account acc;
        private int put;
        
        public putThread(Account acc,int money){
            this.acc=acc;
            this.put=money;
            
        }
        public void run(){
                acc.putMoney(put);
        }
    }
    
    static class takeThread extends Thread{
        private Account acc;
        private int take;
        public takeThread(String threadName,Account acc, int money){
            super(threadName);
            this.acc=acc;
            this.take=money;
            
        }
        public void run(){
             acc.takeMoney(take);
            }
        }
    
        public static void main(String[] args){
             final Account acc=new Account("劉德華");
             Thread thread1=new Thread(new putThread(acc, 30), "執行緒A");
             Thread thread2=new takeThread("執行緒B", acc, 50);
             Thread thread3=new Thread(new putThread(acc, 60), "執行緒C");
             Thread thread4=new takeThread("執行緒D", acc, 70);
             Thread thread5=new takeThread("執行緒E", acc, 20);
             Thread thread6=new Thread(new putThread(acc, 10), "執行緒F");
             Thread thread7=new Thread(new putThread(acc, 30), "執行緒G");
             thread1.start();
             thread2.start();
             thread3.start();
             thread4.start();
             thread5.start();
             thread6.start();
             thread7.start();
             
             
        }
    
}

執行後的結果:

執行緒A存款後,餘額為:130
執行緒G存款後,餘額為:160
執行緒F存款後,餘額為:170
執行緒E取款後,餘額為:150
執行緒D取款後,餘額為:80
執行緒C存款後,餘額為:140
執行緒B取款後,餘額為:90

反面教材,不同步的情況,也就是去掉putMoney(int money)和takeMoney(int money)方法的synchronized修飾符,然後執行程式,結果如下:

執行緒B取款後,餘額為:10
執行緒E取款後,餘額為:-10
執行緒F存款後,餘額為:0
執行緒A存款後,餘額為:10
執行緒D取款後,餘額為:10
執行緒G存款後,餘額為:90
執行緒C存款後,餘額為:90
很顯然,上面的結果是錯誤的,導致錯誤的原因是多個執行緒併發訪問了競爭資源acc,並對acc的屬性做了改動。可見同步的重要性。
 
注意:
透過前文可知,執行緒退出同步方法時將釋放掉方法所屬物件的鎖,但還應該注意的是,同步方法中還可以使用特定的方法對執行緒進行排程。這些方法來自於java.lang.Object類。
void notify()    
                    喚醒在此物件監視器上等待的單個執行緒。    
void notifyAll()    
                    喚醒在此物件監視器上等待的所有執行緒。    
void wait()    
                    導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法。    
void wait(long timeout)    
                    導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。    
void wait(long timeout, int nanos)    
                    導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。 

結合以上方法,處理多執行緒同步與互斥問題非常重要,著名的生產者-消費者例子就是一個經典的例子,任何語言多執行緒必學的例子。

g、同步塊

對於同步,除了同步方法外,還可以使用同步程式碼塊,有時候同步程式碼塊會帶來比同步方法更好的效果。
追其同步的根本的目的,是控制競爭資源的正確的訪問,因此只要在訪問競爭資源的時候保證同一時刻只能一個執行緒訪問即可,因此Java引入了同步程式碼快的策略,以提高效能。
在上個例子的基礎上,對putMoney(int money)和takeMoney(int money)方法做了改動,由同步方法改為同步程式碼塊模式,程式的執行邏輯並沒有問題。
改動後的Account類如下:
static class Account{
           private String Id;
           private int remain;
           public Account(){
               
           }
           public Account(String id){
               this.setId(id);
               this.remain=100;
           }
           public Account(String id, int initRemain ){
               this.setId(id);
               this.remain=initRemain;
           }
           
           public int getRemain(){
               return this.remain;
           }
           public void setRemain(int r){
               this.remain=r;
           }
           
            /**
             * 存款的方法
             *
             */
         public void putMoney(int money){
             try{
             Thread.sleep(10L);
             synchronized (this) {
               this.remain+=money;
             System.out.println(Thread.currentThread().getName()+"存款後,餘額為:"+this.remain);
                }
             Thread.sleep(10L);
             }catch(InterruptedException ine){
                 ine.printStackTrace();
         }
         }
         
         /**
             * 取款的方法
             *
             */
         public void takeMoney(int money){
             try{
             Thread.sleep(10L);
             synchronized(this){
             this.remain-=money;
             System.out.println(Thread.currentThread().getName()+"取款後,餘額為:"+this.remain);
             }
             Thread.sleep(10L);
             }catch(InterruptedException ine){
                 ine.printStackTrace();
         }
         }
        public String getId() {
            return Id;
        }
        public void setId(String id) {
            Id = id;
        }
    }     

三、併發協作

a、經典的生產者消費者模型

對於多執行緒程式來說,不管任何程式語言,生產者和消費者模型都是最經典的。就像學習每一門程式語言一樣,Hello World!都是最經典的例子。
 
實際上,準確說應該是“生產者-消費者-倉儲”模型,離開了倉儲,生產者消費者模型就顯得沒有說服力了。
對於此模型,應該明確一下幾點:
  1. 生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
  2. 消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
  3. 當消費者發現倉儲沒產品可消費時候會通知生產者生產。
  4. 生產者在生產出可消費產品時候,應該通知等待的消費者去消費。
此模型將要結合java.lang.Object的wait與notify、notifyAll方法來實現以上的需求。這是非常重要的。
package MultiThread;

/**
 * 此處模擬的為信用卡賬戶
 * */
public class ProducerConsumerModel2 {

     
    /**
     * 併發協作:生產消費者模型
     * @author donghe
     *
     */
    static class Warehouse{
           private int max_size=100; //最大庫存量
           private int currentNum;   //當前庫存數
           public Warehouse(){
               
           }
           public Warehouse(int capcity){
               this.currentNum=capcity;
           }

        public int getMax_size() {
            return max_size;
        }
        public void setMax_size(int max_size) {
            this.max_size = max_size;
        }
        public int getCurrentNum() {
            return currentNum;
        }
        public void setCurrentNum(int currentNum) {
            this.currentNum = currentNum;
        }
            /**
             * 生產指定數量的產品
             *
             */
         public synchronized void produce(int needNum){
                 while(this.currentNum+needNum>this.max_size){
                     System.out.println("要生產的產品數量"+needNum+"超過倉庫剩餘庫存容量"+(this.max_size-this.currentNum)+",生產任務暫停執行!");
                    
                     try {
                        //當然生產執行緒等待
                        this.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                 }
                 //滿足生產條件,則進行生產,這裡僅僅更改了倉庫中的庫存量
                 this.currentNum+=needNum;
                System.out.println("生產了"+needNum+"個產品,生產後當前倉庫產品總數為"+this.currentNum);
              //喚醒在此物件監視器上等待的所有執行緒
                 this.notifyAll(); 
         }
         
         /**
             * 消費執行數量的產品
             *
             */
         public synchronized void consume(int needNum){
                
                while(this.currentNum<needNum){
                     System.out.println("要消費的產品數量"+needNum+"超過倉庫當前總的產品數"+this.currentNum+",消費任務暫停執行!");
                    //當然生產執行緒等待
                     try {
                        this.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                 }
             
            
                //滿足生產條件,則進行生產,這裡僅僅更改了倉庫中的庫存量
                 this.currentNum-=needNum;
                System.out.println("消費了"+needNum+"個產品,消費後當前倉庫的產品總數為"+this.currentNum);
                //喚醒在此物件監視器上等待的所有執行緒
                 this.notifyAll();
             }
    

}
    static class Producer extends Thread{
        private Warehouse wh;
        private int put;
        
        public Producer(String threadName,Warehouse warehouse,int needNum){
            super(threadName);
            this.wh=warehouse;
            this.put=needNum;
            
        }
        public void run(){
                wh.produce(put);
        }
    }
    
    static class Consumer extends Thread{
        private Warehouse wh;
        private int take;
        public Consumer(String threadName,Warehouse warehouse, int needNum){
            super(threadName);
            this.wh=warehouse;
            this.take=needNum;
            
        }
        public void run(){
             wh.consume(take);
            }
        }
    
    
    
        public static void main(String[] args){
            //倉庫中的初始產品數
             final Warehouse wh=new Warehouse(50);
             Thread p1=new Producer("生產執行緒1", wh, 60);
             Thread c1=new Consumer("消費執行緒1", wh, 20);
             Thread c2=new Consumer("消費執行緒2", wh, 80);
             Thread c3=new Consumer("消費執行緒3", wh, 60);
             Thread p2=new Producer("生產執行緒2", wh, 30);
             Thread p3=new Producer("生產執行緒3", wh, 10);
             Thread p4=new Producer("生產執行緒4", wh, 50);
             
             p1.start();
             c1.start();
             c2.start();
             c3.start();
             p2.start();
             p3.start();
             p4.start();
              
             
        }
    
}

執行結果:

要消費的產品數量80超過倉庫當前總的產品數50,消費任務暫停執行!
生產了50個產品,生產後當前倉庫產品總數為100
消費了80個產品,消費後當前倉庫的產品總數為20
要消費的產品數量60超過倉庫當前總的產品數20,消費任務暫停執行!
生產了60個產品,生產後當前倉庫產品總數為80
消費了60個產品,消費後當前倉庫的產品總數為20
消費了20個產品,消費後當前倉庫的產品總數為0
生產了30個產品,生產後當前倉庫產品總數為30
生產了10個產品,生產後當前倉庫產品總數為40
說明:
對於本例,要說明的是當發現不能滿足生產或者消費條件的時候,呼叫物件的wait方法,wait方法的作用是釋放當前執行緒的所獲得的鎖,並呼叫物件的notifyAll() 方法,通知(喚醒)該物件上其他等待執行緒,使得其繼續執行。這樣,整個生產者、消費者執行緒得以正確的協作執行。
notifyAll() 方法,起到的是一個通知作用,不釋放鎖,也不獲取鎖。只是告訴該物件上等待的執行緒“可以競爭執行了,都醒來去執行吧”。
 
本例僅僅是生產者消費者模型中最簡單的一種表示,然而,如果消費者消費的倉儲量達不到滿足,而又沒有生產者,則程式會一直處於等待狀態,如下圖所示:
這當然是不對的。實際上可以將此例進行修改,根據消費驅動生產,同時生產兼顧倉庫,如果倉不滿就生產,並對每次最大消費量做個限制,這樣就不存在此問題了,當然這樣的例子更復雜,更難以說明這樣一個簡單模型。

b、死鎖

執行緒發生死鎖可能性很小,即使看似可能發生死鎖的程式碼,在執行時發生死鎖的可能性也是小之又小。
發生死鎖的原因一般是兩個物件的鎖相互等待造成的。
下面是死鎖的一個完整例子:
package MultiThread;

/** 
* 併發協作-死鎖 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                DeadlockRisk dead = new DeadlockRisk(); 
                MyThread t1 = new MyThread(dead, 1, 2); 
                MyThread t2 = new MyThread(dead, 3, 4); 
                MyThread t3 = new MyThread(dead, 5, 6); 
                MyThread t4 = new MyThread(dead, 7, 8); 

                t1.start(); 
                t2.start(); 
                t3.start(); 
                t4.start(); 
        } 

} 

class MyThread extends Thread { 
        private DeadlockRisk dead; 
        private int a, b; 

        MyThread(DeadlockRisk dead, int a, int b) { 
                this.dead = dead; 
                this.a = a; 
                this.b = b; 
        } 

        @Override 
        public void run() { 
                dead.read(); 
                dead.write(a, b); 
        } 
} 

class DeadlockRisk { 
        private static class Resource { 
                public int value; 
             
        } 

        private Resource resourceA = new Resource(); 
        private Resource resourceB = new Resource(); 

        public int read() { 
                synchronized (resourceA) { 
                        System.out.println("read():" + Thread.currentThread().getName() + "獲取了resourceA的鎖!"); 
                        synchronized (resourceB) { 
                                System.out.println("read():" + Thread.currentThread().getName() + "獲取了resourceB的鎖!");
                                System.out.println("讀成功! resourceA.value="+resourceA.value+"\tresourceB.value="+resourceB.value);
                                return resourceB.value + resourceA.value; 
                        } 
                } 
        } 

        public void write(int a, int b) { 
                synchronized (resourceB) { 
                        System.out.println("write():" + Thread.currentThread().getName() + "獲取了resourceB的鎖!"); 
                        synchronized (resourceA) { 
                                System.out.println("write():" + Thread.currentThread().getName() + "獲取了resourceA的鎖!"); 
                                resourceA.value = a; 
                                resourceB.value = b; 
                                System.out.println("寫成功!");
                        } 
                } 
        } 
}

執行結果為:

由執行結果可以看出,寫執行緒Thread-0獲得resourceB的鎖,讀執行緒獲得了Thread-2的鎖,兩者都因無法獲得被對方佔有,而不釋放的鎖,使程式執行進入死鎖,執行到圖中某處停止。

c、volatile關鍵字

Java 語言包含兩種內在的同步機制:synchronized同步塊(或方法)和 volatile 變數。這兩種機制的提出都是為了實現程式碼執行緒的安全性。其中 volatile 變數的同步性較差(但有時它更簡單並且開銷更低),而且其使用也更容易出錯。
談及到volatile關鍵字,不得不提的一篇文章是:《Java 理論與實踐: 正確使用 Volatile 變數》,這篇文章對volatile關鍵字的用法做了相當精闢的闡述。
之所以要單獨提出volatile這個不常用的關鍵字原因是這個關鍵字在高效能的多執行緒程式中也有很重要的用途,只是這個關鍵字用不好會出很多問題。
 
    首先考慮一個問題,為什麼變數需要volatile來修飾呢?
要搞清楚這個問題,首先應該明白計算機內部都做什麼了。比如做了一個i++操作,計算機內部做了三次處理:讀取-修改-寫入。
同樣,對於一個long型資料,做了個賦值操作,在32系統下需要經過兩步才能完成,先修改低32位,然後修改高32位。
假想一下,當將以上的操作放到一個多執行緒環境下操作時候,有可能出現的問題,是這些步驟執行了一部分,而另外一個執行緒就已經引用了變數值,這樣就導致了讀取髒資料的問題。
透過這個設想,就不難理解volatile關鍵字了。
volatile可以用在任何變數前面,但不能用於final變數前面,因為final型的變數是禁止修改的。也不存線上程安全的問題。
Java 語言中的 volatile 變數可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 變數所需的編碼較少,並且執行時開銷也較少,但是它所能實現的功能也僅是 synchronized 的一部分。
更多的內容,請參看::《Java 理論與實踐: 正確使用 Volatile 變數》一文,寫得很好。
 

相關文章