Java執行緒新特徵——Java併發庫

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

一、執行緒池

  Sun在Java5中,對Java執行緒的類庫做了大量的擴充套件,其中執行緒池就是Java5的新特徵之一,除了執行緒池之外,還有很多多執行緒相關的內容,為多執行緒的程式設計帶來了極大便利。為了編寫高效穩定可靠的多執行緒程式,執行緒部分的新增內容顯得尤為重要。
    有關Java5執行緒新特徵的內容全部在java.util.concurrent下面,裡面包含數目眾多的介面和類,熟悉這部分API特徵是一項艱難的學習過程。當然新特徵對做多執行緒程式沒有必須的關係,在java5之前通用可以寫出很優秀的多執行緒程式。只是代價不一樣而已。
執行緒池的基本思想還是一種物件池的思想,開闢一塊記憶體空間,裡面存放了眾多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆建立執行緒物件所帶來的效能開銷,節省了系統的資源。
    在Java5之前,要實現一個執行緒池是相當有難度的,現在Java5為我們做好了一切,我們只需要按照提供的API來使用,即可享受執行緒池帶來的極大便利。
Java5的執行緒池分好多種:固定尺寸的執行緒池可變尺寸連線池
 
在使用執行緒池之前,必須知道如何去建立一個執行緒池,在Java5中,需要了解的是java.util.concurrent.Executors類的API,這個類提供大量建立連線池的靜態方法,是必須掌握的。

a、固定大小的執行緒池

import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 

/** 
* Java執行緒:執行緒池- 
* 
* */ 
public class Test { 
        public static void main(String[] args) { 
                //建立一個可重用固定執行緒數的執行緒池 
                ExecutorService pool = Executors.newFixedThreadPool(2); 
                //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面 
                Thread t1 = new MyThread(); 
                Thread t2 = new MyThread(); 
                Thread t3 = new MyThread(); 
                Thread t4 = new MyThread(); 
                Thread t5 = new MyThread(); 
                //將執行緒放入池中進行執行 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                pool.execute(t4); 
                pool.execute(t5); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyThread extends Thread{ 
        @Override 
        public void run() { 
                System.out.println(Thread.currentThread().getName()+"正在執行..."); 
        } 
}

執行結果:

pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
pool-1-thread-2正在執行...
pool-1-thread-1正在執行...
pool-1-thread-2正在執行...

 如果將執行緒池的大小改為4,則執行結果如下:

pool-1-thread-2正在執行...
pool-1-thread-3正在執行...
pool-1-thread-3正在執行...
pool-1-thread-2正在執行...
pool-1-thread-1正在執行...

 

b、單任務執行緒池

在上例的基礎上改一行建立pool物件的程式碼為:

 //建立一個使用單個 worker 執行緒的 Executor,以無界佇列方式來執行該執行緒。 
ExecutorService pool = Executors.newSingleThreadExecutor(); 

則,執行結果為:

pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
對於以上兩種連線池,大小都是固定的,當要加入的池的執行緒(或者任務)超過池最大尺寸時候,則入此執行緒池需要排隊等待。
一旦池中有執行緒完畢,則排隊等待的某個執行緒會入池執行。

 c、可變尺寸的執行緒池

 與上面的類似,只是改動下pool的建立方式:

  //建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。 
   ExecutorService pool = Executors.newCachedThreadPool(); 

執行結果如下:

pool-1-thread-1正在執行...
pool-1-thread-5正在執行...
pool-1-thread-4正在執行...
pool-1-thread-3正在執行...
pool-1-thread-2正在執行...

d、延遲連線池

package concurrent;
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/** 
* Java執行緒:執行緒池- 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立一個執行緒池,它可那排在給定延遲後執行命令或者定期地執行
                ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
                //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面 
                Thread t1 = new MyThread(); 
                Thread t2 = new MyThread(); 
                Thread t3 = new MyThread(); 
                Thread t4 = new MyThread(); 
                Thread t5 = new MyThread(); 
                //將執行緒放入池中進行執行 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                 //使用延遲執行風格的方法
                pool.schedule(t4, 5, TimeUnit.SECONDS);
                pool.schedule(t5, 10, TimeUnit.SECONDS);
                
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyThread extends Thread{ 
        @Override 
        public void run() { 
                System.out.println(Thread.currentThread().getName()+"正在執行..."); 
        } 
}

e、單任務連線執行緒池

在d的程式碼基礎上,做改動

 //建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。 
 ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

執行時,會發現,t4延遲5s後得到執行,t5延遲10s後得到執行。執行結果如下:

pool-1-thread-2正在執行...
pool-1-thread-1正在執行...
pool-1-thread-1正在執行...
pool-1-thread-2正在執行...
pool-1-thread-1正在執行...

f、自定義執行緒池

package concurrent;
import java.util.concurrent.ArrayBlockingQueue; 
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.ThreadPoolExecutor; 
import java.util.concurrent.TimeUnit; 

/** 
* Java執行緒:執行緒池-自定義執行緒池 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立等待佇列 
                BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20); 
                //建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。 
                ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2,TimeUnit.MILLISECONDS,bqueue); 
                //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面 
                Thread t1 = new MyThread(); 
                Thread t2 = new MyThread(); 
                Thread t3 = new MyThread(); 
                Thread t4 = new MyThread(); 
                Thread t5 = new MyThread(); 
                Thread t6 = new MyThread(); 
                Thread t7 = new MyThread(); 
                //將執行緒放入池中進行執行 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                pool.execute(t4); 
                pool.execute(t5); 
                pool.execute(t6); 
                pool.execute(t7); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyThread extends Thread { 
        @Override 
        public void run() { 
                System.out.println(Thread.currentThread().getName() + "正在執行..."); 
                try { 
                        Thread.sleep(100L); 
                } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                } 
        } 
}

執行結構如下:
建立自定義執行緒池的構造方法很多,本例中引數的含義如下:

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)
用給定的初始引數和預設的執行緒工廠及處理程式建立新的 ThreadPoolExecutor。使用 Executors 工廠方法之一比使用此通用構造方法方便得多。 
引數:
corePoolSize - 池中所儲存的執行緒數,包括空閒執行緒。 
maximumPoolSize - 池中允許的最大執行緒數。 
keepAliveTime - 當執行緒數大於核心時,此為終止前多餘的空閒執行緒等待新任務的最長時間。 
unit - keepAliveTime 引數的時間單位。 
workQueue - 執行前用於保持任務的佇列。此佇列僅保持由 execute 方法提交的 Runnable 任務。 
丟擲:
IllegalArgumentException - 如果 corePoolSize 或 keepAliveTime 小於零,或者 maximumPoolSize 小於或等於零,或者 corePoolSize 大於 maximumPoolSize。 
NullPointerException - 如果 workQueue 為 null
 
自定義連線池稍微麻煩些,不過透過建立的ThreadPoolExecutor執行緒池物件,可以獲取到當前執行緒池的尺寸、正在執行任務的執行緒數、工作佇列等等。

二、有返回值的執行緒

在Java5之前,執行緒是沒有返回值的,常常為了“有”返回值,破費周折,而且程式碼很不好寫。或者乾脆繞過這道坎,走別的路了。現在Java終於有可返回值的執行緒了。
 可返回值的任務必須實現Callable介面,類似的,無返回值的任務必須實現Runnable介面。
 執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了。

 下面是一個簡單的例子:

 

package MultiThread;
import java.util.concurrent.*; 

/** 
* Java執行緒:有返回值的執行緒 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) throws ExecutionException, InterruptedException { 
                //建立一個執行緒池 
                ExecutorService pool = Executors.newFixedThreadPool(2); 
                //建立兩個有返回值的任務 
                Callable<String> c1 = new MyCallable("A"); 
                Callable<String> c2 = new MyCallable("B"); 
                //執行任務並獲取Future物件 
                Future<String> f1 = pool.submit(c1); 
                Future<String> f2 = pool.submit(c2); 
                //從Future物件上獲取任務的返回值,並輸出到控制檯 
                System.out.println(">>>"+f1.get().toString()); 
                System.out.println(">>>"+f2.get().toString()); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyCallable implements Callable<String>{ 
        private String oid; 

        MyCallable(String oid) { 
                this.oid = oid; 
        } 

        @Override 
        public String call() throws Exception { 
                return oid+"任務返回的內容"; 
        } 
}

執行結果:

>>>A任務返回的內容
>>>B任務返回的內容

比較簡單,要深入瞭解還需要看Callable和Future介面的API啊。

三、併發庫的鎖

在Java5中,專門提供了鎖物件,利用鎖可以方便的實現資源的封鎖,用來控制對競爭資源併發訪問的控制,這些內容主要集中在java.util.concurrent.locks 包下面,裡面有三個重要的介面Condition、Lock、ReadWriteLock。

介面摘要
Condition ConditionObject 監視器方法(waitnotifynotifyAll)分解成截然不同的物件,以便透過將這些物件與任意 Lock 實現組合使用,為每個物件提供多個等待 set(wait-set)。
Lock Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
ReadWriteLock ReadWriteLock 維護了一對相關的,一個用於只讀操作,另一個用於寫入操作。

 

 

 

 

 有關鎖的介紹,API文件解說很多,看得很煩,還是看個例子再看文件比較容易理解

 a、普通鎖

package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReentrantLock; 

/** 
* Java執行緒:鎖 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立併發訪問的賬戶 
                MyCount myCount = new MyCount("95599200901215522", 10000); 
                //建立一個鎖物件 
                Lock lock = new ReentrantLock(); 
                //建立一個執行緒池 
                ExecutorService pool = Executors.newCachedThreadPool(); 
                //建立一些併發訪問使用者,一個信用卡,存的存,取的取,好熱鬧啊 
                UserThread ut1 = new UserThread("取款執行緒1", myCount, -4000, lock); 
                UserThread ut2 = new UserThread("存款執行緒1", myCount, 6000, lock); 
                UserThread ut3 = new UserThread("取款執行緒2", myCount, -8000, lock); 
                UserThread ut4 = new UserThread("存款執行緒2", myCount, 800, lock); 
                //線上程池中執行各個使用者的操作 
                pool.execute(ut1); 
                pool.execute(ut2); 
                pool.execute(ut3); 
                pool.execute(ut4); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

/** 
* 信用卡的使用者 執行緒
* 多個使用者執行緒操作該信用卡
*/ 
class UserThread implements Runnable { 
        private String threadName;                //使用者執行緒 
        private MyCount myCount;        //所要操作的賬戶 
        private int iocash;                 //操作的金額,當然有正負之分了 
        private Lock myLock;                //執行操作所需的鎖物件 

        UserThread(String name, MyCount myCount, int iocash, Lock myLock) { 
                this.threadName = name; 
                this.myCount = myCount; 
                this.iocash = iocash; 
                this.myLock = myLock; 
        } 

        public void run() { 
                //獲取鎖 
                myLock.lock(); 
                //執行現金業務 
                System.out.println(threadName + "正在操作" + myCount + "賬戶,操作金額為" + iocash + ",當前金額為" + myCount.getCash()); 
                myCount.setCash(myCount.getCash() + iocash); 
                System.out.println("\t操作成功,操作金額為" + iocash + ",當前金額為" + myCount.getCash()); 
                //釋放鎖,否則別的執行緒沒有機會執行了 
                myLock.unlock(); 
        } 
} 

/** 
* 信用卡賬戶,可隨意透支 
*/ 
class MyCount { 
        private String oid;         //賬號 
        private int cash;             //賬戶餘額 

        MyCount(String oid, int cash) { 
                this.oid = oid; 
                this.cash = cash; 
        } 

        public String getOid() { 
                return oid; 
        } 

        public void setOid(String oid) { 
                this.oid = oid; 
        } 

        public int getCash() { 
                return cash; 
        } 

        public void setCash(int cash) { 
                this.cash = cash; 
        } 

        @Override 
        public String toString() { 
                return "MyCount{" + 
                                "oid='" + oid + '\'' + 
                                ", cash=" + cash + 
                                '}'; 
        } 
}

執行結果:

取款執行緒1正在操作MyCount{oid='95599200901215522', cash=10000}賬戶,操作金額為-4000,當前金額為10000
    操作成功,操作金額為-4000,當前金額為6000
存款執行緒1正在操作MyCount{oid='95599200901215522', cash=6000}賬戶,操作金額為6000,當前金額為6000
    操作成功,操作金額為6000,當前金額為12000
存款執行緒2正在操作MyCount{oid='95599200901215522', cash=12000}賬戶,操作金額為800,當前金額為12000
    操作成功,操作金額為800,當前金額為12800
取款執行緒2正在操作MyCount{oid='95599200901215522', cash=12800}賬戶,操作金額為-8000,當前金額為12800
    操作成功,操作金額為-8000,當前金額為4800
從上面的輸出可以看到,利用鎖物件太方便了,比直接在某個不知情的物件上用鎖清晰多了。但一定要注意的是,在獲取了鎖物件後,用完後應該儘快釋放鎖,以便別的等待該鎖的執行緒有機會去執行。

b、讀寫鎖

 在a中提到了Lock介面以及物件,使用它可以很優雅的控制了競爭資源的安全訪問,但是這種鎖不區分讀寫,稱這種鎖為普通鎖。為了提高效能,Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,在一定程度上提高了程式的執行效率。
Java中讀寫鎖有個介面java.util.concurrent.locks.ReadWriteLock,也有具體的實現ReentrantReadWriteLock,詳細的API可以檢視JavaAPI文件。
下面這個例子是在文例子的基礎上,將普通鎖改為讀寫鎖,並新增賬戶餘額查詢的功能,程式碼如下:
 
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock; 
import java.util.concurrent.locks.ReentrantReadWriteLock;

/** 
* Java執行緒:鎖 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立併發訪問的賬戶 
                MyCount myCount = new MyCount("95599200901215522", 10000); 
                //建立一個鎖物件 
                ReadWriteLock lock = new ReentrantReadWriteLock(); 
                //建立一個執行緒池 
                ExecutorService pool = Executors.newCachedThreadPool(); 
                //建立一些併發訪問使用者執行緒,一個信用卡,存的存,取的取,好熱鬧啊 
                UserThread ut1 = new UserThread("取款執行緒1", myCount, -4000, lock,false); 
                UserThread ut2 = new UserThread("存款執行緒1", myCount, 6000, lock,false); 
                UserThread ut3 = new UserThread("取款執行緒2", myCount, -8000, lock,false); 
                UserThread ut4 = new UserThread("存款執行緒2", myCount, 800, lock,false); 
                UserThread ut5 = new UserThread("查詢", myCount, 0, lock,true);
                //線上程池中執行各個使用者的操作 
                pool.execute(ut1); 
                pool.execute(ut2); 
                pool.execute(ut3); 
                pool.execute(ut4); 
                pool.execute(ut5);
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

/** 
* 信用卡的使用者 執行緒
* 多個使用者執行緒操作該信用卡
*/ 
class UserThread implements Runnable { 
        private String threadName;                //使用者執行緒 
        private MyCount myCount;        //所要操作的賬戶 
        private int iocash;                 //操作的金額,當然有正負之分了 
        private ReadWriteLock myLock;                //執行操作所需的鎖物件 
        private boolean ischeck;      //是否查詢

        UserThread(String name, MyCount myCount, int iocash, ReadWriteLock myLock,boolean ischeck) { 
                this.threadName = name; 
                this.myCount = myCount; 
                this.iocash = iocash; 
                this.myLock = myLock; 
                this.ischeck=ischeck;
                
        } 

        public void run() {
            
            if(ischeck){
                //獲取讀鎖 
                myLock.readLock().lock(); 
                //執行查詢
                System.out.println("讀:"+threadName + "正在查詢" + myCount + "賬戶,,當前金額為" + myCount.getCash()); 
               //釋放獲取到的讀鎖
                myLock.readLock().unlock();
            }else{
                //獲取寫鎖
                myLock.writeLock().lock();
                
                 myCount.setCash(myCount.getCash() + iocash); 
                System.out.println("寫:"+threadName+"操作成功,操作金額為" + iocash + ",當前金額為" + myCount.getCash()); 
                //釋放鎖獲取到的寫鎖
                myLock.writeLock().unlock(); 
            }
        } 
} 

/** 
* 信用卡賬戶,可隨意透支 
*/ 
class MyCount { 
        private String oid;         //賬號 
        private int cash;             //賬戶餘額 

        MyCount(String oid, int cash) { 
                this.oid = oid; 
                this.cash = cash; 
        } 

        public String getOid() { 
                return oid; 
        } 

        public void setOid(String oid) { 
                this.oid = oid; 
        } 

        public int getCash() { 
                return cash; 
        } 

        public void setCash(int cash) { 
                this.cash = cash; 
        } 

        @Override 
        public String toString() { 
                return "MyCount{" + 
                                "oid='" + oid + '\'' + 
                                ", cash=" + cash + 
                                '}'; 
        } 
}

 執行結果:

寫:取款執行緒1操作成功,操作金額為-4000,當前金額為6000
寫:取款執行緒2操作成功,操作金額為-8000,當前金額為-2000
寫:存款執行緒1操作成功,操作金額為6000,當前金額為4000
讀:查詢正在查詢MyCount{oid='95599200901215522', cash=4000}賬戶,,當前金額為4000
寫:存款執行緒2操作成功,操作金額為800,當前金額為4800

在實際開發中,最好在能用讀寫鎖的情況下使用讀寫鎖,而不要用普通鎖,以求更好的效能。

 四、訊號量

Java的訊號量實際上是一個功能完畢的計數器,對控制一定資源的消費與回收有著很重要的意義,訊號量常常用於多執行緒的程式碼中,並能監控有多少數目的執行緒等待獲取資源,並且透過訊號量可以得知可用資源的數目等等,這裡總是在強調“數目”二字,但不能指出來有哪些在等待,哪些資源可用。
 因此,本人認為,這個訊號量類如果能返回數目,還能知道哪些物件在等待,哪些資源可使用,就非常完美了,僅僅拿到這些概括性的數字,對精確控制意義不是很大。目前還沒想到更好的用法。

下面是一個簡單的例子:

package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; 

/** 
* Java執行緒:訊號量 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) { 
                MyPool myPool = new MyPool(20); 
                //建立執行緒池 
                ExecutorService threadPool = Executors.newFixedThreadPool(2); 
                MyThread t1 = new MyThread("任務A", myPool, 3); 
                MyThread t2 = new MyThread("任務B", myPool, 12); 
                MyThread t3 = new MyThread("任務C", myPool, 7); 
                //線上程池中執行任務 
                threadPool.execute(t1); 
                threadPool.execute(t2); 
                threadPool.execute(t3); 
                //關閉池 
                threadPool.shutdown(); 
        } 
} 

/** 
* 一個池 
*/ 
class MyPool { 
        private Semaphore sp;     //池相關的訊號量 

        /** 
         * 池的大小,這個大小會傳遞給訊號量 
         * 
         * @param size 池的大小 
         */ 
        MyPool(int size) { 
                this.sp = new Semaphore(size); 
        } 

        public Semaphore getSp() { 
                return sp; 
        } 

        public void setSp(Semaphore sp) { 
                this.sp = sp; 
        } 
} 

class MyThread extends Thread { 
        private String threadName;            //執行緒的名稱 
        private MyPool pool;                        //自定義池 
        private int x;                                    //申請訊號量的大小 

        MyThread(String threadName, MyPool pool, int x) { 
                this.threadName = threadName; 
                this.pool = pool; 
                this.x = x; 
        } 

        public void run() { 
                try { 
                        //從此訊號量獲取給定數目的許可 
                        pool.getSp().acquire(x); 
                        //todo:也許這裡可以做更復雜的業務 
                        System.out.println(threadName + "成功獲取了" + x + "個許可!"); 
                } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                } finally { 
                        //釋放給定數目的許可,將其返回到訊號量。 
                        pool.getSp().release(x); 
                        System.out.println(threadName + "釋放了" + x + "個許可!"); 
                } 
        } 
}

執行結果:

任務A成功獲取了3個許可!
任務B成功獲取了12個許可!
任務B釋放了12個許可!
任務C成功獲取了7個許可!
任務A釋放了3個許可!
任務C釋放了7個許可!

從結果可以看出,訊號量僅僅是對池資源進行監控,但不保證執行緒的安全,因此,在使用時候,應該自己控制執行緒的安全訪問池資源。

五、阻塞佇列

    阻塞佇列是Java5執行緒新特徵中的內容,Java定義了阻塞佇列的介面java.util.concurrent.BlockingQueue,阻塞佇列的概念是,一個指定長度的佇列,如果佇列滿了,新增新元素的操作會被阻塞等待,直到有空位為止。同樣,當佇列為空時候,請求佇列元素的操作同樣會阻塞等待,直到有可用元素為止。
有了這樣的功能,就為多執行緒的排隊等候的模型實現開闢了便捷通道,非常有用。
java.util.concurrent.BlockingQueue繼承了java.util.Queue介面,可以參看API文件。
下面給出一個簡單應用的例子:
package MultiThread;
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.ArrayBlockingQueue; 

/** 
* Java執行緒:併發庫-阻塞佇列 
* 
* 
*/ 
public class Test { 
        public static void main(String[] args) throws InterruptedException { 
                BlockingQueue<Integer> bqueue = new ArrayBlockingQueue<Integer>(20); 
                for (int i = 0; i < 30; i++) { 
                        //將指定元素新增到此佇列中,如果沒有可用空間,將一直等待(如果有必要)。 
                        bqueue.put(i); 
                        System.out.println("向阻塞佇列中新增了元素:" + i); 
                } 
                System.out.println("程式到此執行結束,即將退出----"); 
        } 
}

執行結果:

由於阻塞佇列的大小為20個,當超過這個數目,又沒有元素出佇列的時候,佇列將會阻塞。到後來的某一個時刻,程式將阻塞佇列中的元素出佇列,後面的元素才可以進佇列。

六、阻塞棧

對於阻塞棧,與阻塞佇列相似。不同點在於棧是“後入先出”的結構,每次操作的是棧頂,而佇列是“先進先出”的結構,每次操作的是佇列頭。
這裡要特別說明一點的是,阻塞棧是Java6的新特徵。、
Java為阻塞棧定義了介面:java.util.concurrent.BlockingDeque,其實現類也比較多,具體可以檢視JavaAPI文件。
 
下面看一個簡單例子:
package MultiThread;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
class HelloWorldThread {
    
    /** 
    * Java執行緒:併發庫-阻塞棧
    * 
    * 
    */ 
    
    public static void main(String[] args) {
      BlockingDeque<Integer> bstack=new LinkedBlockingDeque<Integer>(20) ;
      
      for(int i=0;i<30;i++){
          //將指定元素新增到阻塞棧中,如果沒有可用空間,將一直等待(如果有必要)。 
          bstack.push(i);
          System.out.println("向阻塞棧中新增了元素:" + i); 
          if(bstack.size()==20){

                  System.out.println("佇列滿,彈出"+bstack.pop()); 
                  System.out.println("佇列滿,彈出"+bstack.pop()); 
                  System.out.println("佇列滿,彈出"+bstack.pop()); 
          }
      }
      
      System.out.println("程式到此執行結束,即將退出----"); 
    }

}
 

程式的執行結果和阻塞佇列的執行結果一樣,程式並沒結束,二是阻塞住了,原因是棧已經滿了,後面追加元素的操作都被阻塞了。

七、條件變數

    條件變數是Java5執行緒中很重要的一個概念,顧名思義,條件變數就是表示條件的一種變數。但是必須說明,這裡的條件是沒有實際含義的,僅僅是個標記而已,並且條件的含義往往透過程式碼來賦予其含義。這裡的條件和普通意義上的條件表示式有著天壤之別。條件變數都實現了java.util.concurrent.locks.Condition介面,條件變數的例項化是透過一個Lock物件上呼叫newCondition()方法來獲取的,這樣,條件就和一個鎖物件繫結起來了。因此,Java中的條件變數只能和鎖配合使用,來控制併發程式訪問競爭資源的安全。
    條件變數的出現是為了更精細控制執行緒等待與喚醒,在Java5之前,執行緒的等待與喚醒依靠的是Object物件的wait()和notify()/notifyAll()方法,這樣的處理不夠精細。而在Java5中,一個鎖可以有多個條件,每個條件上可以有多個執行緒等待,透過呼叫await()方法,可以讓執行緒在該條件下等待。當呼叫signalAll()方法,又可以喚醒該條件下的等待的執行緒。有關Condition介面的API可以具體參考JavaAPI文件。
       條件變數比較抽象,原因是他不是自然語言中的條件概念,而是程式控制的一種手段。
      下面以一個銀行存取款的模擬程式為例來揭蓋Java多執行緒條件變數的神秘面紗:
有一個賬戶,多個使用者(執行緒)在同時操作這個賬戶,有的存款有的取款,存款隨便存,取款有限制,不能透支,任何試圖透支的操作都將等待裡面有足夠存款才執行操作。
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Condition; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReentrantLock; 

/** 
* Java執行緒:條件變數 
* 
* @author leizhimin 2009-11-5 10:57:29 
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立併發訪問的賬戶 
                MyCount myCount = new MyCount("95599200901215522", 10000); 
                //建立一個執行緒池 
                ExecutorService pool = Executors.newFixedThreadPool(2); 
                Thread t1 = new SaveThread("張三", myCount, 2000); 
                Thread t2 = new SaveThread("李四", myCount, 3600); 
                Thread t3 = new DrawThread("王五", myCount, 2700); 
                Thread t4 = new SaveThread("老張", myCount, 600); 
                Thread t5 = new DrawThread("老牛", myCount, 1300); 
                Thread t6 = new DrawThread("胖子", myCount, 800); 
                //執行各個執行緒 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                pool.execute(t4); 
                pool.execute(t5); 
                pool.execute(t6); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

/** 
* 存款執行緒類 
*/ 
class SaveThread extends Thread { 
        private String name;                //操作人 
        private MyCount myCount;        //賬戶 
        private int x;                            //存款金額 

        SaveThread(String name, MyCount myCount, int x) { 
                this.name = name; 
                this.myCount = myCount; 
                this.x = x; 
        } 

        public void run() { 
                myCount.saving(x, name); 
        } 
} 

/** 
* 取款執行緒類 
*/ 
class DrawThread extends Thread { 
        private String name;                //操作人 
        private MyCount myCount;        //賬戶 
        private int x;                            //存款金額 

        DrawThread(String name, MyCount myCount, int x) { 
                this.name = name; 
                this.myCount = myCount; 
                this.x = x; 
        } 

        public void run() { 
                myCount.drawing(x, name); 
        } 
} 


/** 
* 普通銀行賬戶,不可透支 
*/ 
class MyCount { 
        private String oid;                         //賬號 
        private int cash;                             //賬戶餘額 
        private Lock lock = new ReentrantLock();                //賬戶鎖 
        private Condition _save = lock.newCondition();    //存款條件 
        private Condition _draw = lock.newCondition();    //取款條件 

        MyCount(String oid, int cash) { 
                this.oid = oid; 
                this.cash = cash; 
        } 

        /** 
         * 存款 
         * 
         * @param x    操作金額 
         * @param name 操作人 
         */ 
        public void saving(int x, String name) { 
                lock.lock();                        //獲取鎖 
                if (x > 0) { 
                        cash += x;                    //存款 
                        System.out.println(name + "存款" + x + ",當前餘額為" + cash); 
                } 
                _draw.signalAll();            //喚醒所有取款等待執行緒
                lock.unlock();                    //釋放鎖 
        } 

        /** 
         * 取款 
         * 
         * @param x    操作金額 
         * @param name 操作人 
         */ 
        public void drawing(int x, String name) { 
                lock.lock();                                 //獲取鎖 
                try { 
                        if (cash - x < 0) { 
                                _draw.await();             //阻塞取款操作 
                        } else { 
                                cash -= x;                     //取款 
                                System.out.println(name + "取款" + x + ",當前餘額為" + cash); 
                        } 
                        _save.signalAll();             //喚醒所有存款操作執行緒 
                } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                } finally { 
                        lock.unlock();                     //釋放鎖 
                } 
        } 
}

當然,除了使用併發庫來實現存取款操作,我們也可以使用synchronized的方法、synchronized的程式碼塊來實現。對比並發庫、synchronized方法、synchronized程式碼塊,第一種最靈活,第二種程式碼最簡單,第三種容易犯錯。

八、原子量

     所謂的原子量即操作變數的操作是“原子的”,該操作不可再分,因此是執行緒安全的。為何要使用原子變數呢,原因是多個執行緒對單個變數操作也會引起一些問題。在Java5之前,可以透過volatile、synchronized關鍵字來解決併發訪問的安全問題,但這樣太麻煩。Java5之後,專門提供了用來進行單變數多執行緒併發安全訪問的工具包java.util.concurrent.atomic,其中的類也很簡單。
下面給出一個反面例子(切勿模仿):
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.atomic.AtomicLong; 

/** 
* Java執行緒:新特徵-原子量 
* 
*
*/ 
public class Test { 
        public static void main(String[] args) { 
                ExecutorService pool = Executors.newFixedThreadPool(2); 
                Runnable t1 = new MyRunnable("張三", 2000); 
                Runnable t2 = new MyRunnable("李四", 3600); 
                Runnable t3 = new MyRunnable("王五", 2700); 
                Runnable t4 = new MyRunnable("老張", 600); 
                Runnable t5 = new MyRunnable("老牛", 1300); 
                Runnable t6 = new MyRunnable("胖子", 800); 
                //執行各個執行緒 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                pool.execute(t4); 
                pool.execute(t5); 
                pool.execute(t6); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyRunnable implements Runnable { 
        private static AtomicLong aLong = new AtomicLong(10000);        //原子量,每個執行緒都可以自由操作 
        private String name;                //操作人 
        private int x;                            //運算元額 

        MyRunnable(String name, int x) { 
                this.name = name; 
                this.x = x; 
        } 

        public void run() { 
                System.out.println(name + "執行了" + x + ",當前餘額:" + aLong.addAndGet(x)); 
        } 
}

執行結果一:

李四執行了3600,當前餘額:13600
張三執行了2000,當前餘額:15600
老張執行了600,當前餘額:18900
老牛執行了1300,當前餘額:20200
胖子執行了800,當前餘額:21000
王五執行了2700,當前餘額:18300

執行結果二:

張三執行了2000,當前餘額:12000
王五執行了2700,當前餘額:14700
老張執行了600,當前餘額:15300
老牛執行了1300,當前餘額:16600
胖子執行了800,當前餘額:17400
李四執行了3600,當前餘額:21000

執行結果三:

張三執行了2000,當前餘額:12000 
王五執行了2700,當前餘額:18300 
老張執行了600,當前餘額:18900 
老牛執行了1300,當前餘額:20200 
胖子執行了800,當前餘額:21000 
李四執行了3600,當前餘額:15600 
從執行結果可以看出,雖然使用了原子量,但是程式併發訪問還是有問題,那究竟問題出在哪裡了?
這裡要注意的一點是,原子量雖然可以保證單個變數在某一個操作過程的安全,但無法保證你整個程式碼塊,或者整個程式的安全性。因此,通常還應該使用鎖等同步機制來控制整個程式的安全性。
下面是對上述程式碼的修正:
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.atomic.AtomicLong; 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/** 
* Java執行緒:併發庫-原子量 
* 
*
*/ 
public class Test { 
        public static void main(String[] args) { 
                ExecutorService pool = Executors.newFixedThreadPool(2); 
                Lock lock=new ReentrantLock();
                Runnable t1 = new MyRunnable("張三", 2000,lock); 
                Runnable t2 = new MyRunnable("李四", 3600,lock); 
                Runnable t3 = new MyRunnable("王五", 2700,lock); 
                Runnable t4 = new MyRunnable("老張", 600,lock); 
                Runnable t5 = new MyRunnable("老牛", 1300,lock); 
                Runnable t6 = new MyRunnable("胖子", 800,lock); 
                //執行各個執行緒 
                pool.execute(t1); 
                pool.execute(t2); 
                pool.execute(t3); 
                pool.execute(t4); 
                pool.execute(t5); 
                pool.execute(t6); 
                //關閉執行緒池 
                pool.shutdown(); 
        } 
} 

class MyRunnable implements Runnable { 
        private static AtomicLong aLong = new AtomicLong(10000);        //原子量,每個執行緒都可以自由操作 
        private String name;                //操作人 
        private int x;      //運算元額 
        private Lock lock;

        MyRunnable(String name, int x,Lock lock) { 
                this.name = name; 
                this.x = x; 
                this.lock=lock;
        } 

        public void run() { 
               lock.lock();
                System.out.println(name + "執行了" + x + ",當前餘額:" + aLong.addAndGet(x)); 
                lock.unlock();
        } 
}

執行結果:

張三執行了2000,當前餘額:12000
李四執行了3600,當前餘額:15600
王五執行了2700,當前餘額:18300
老張執行了600,當前餘額:18900
老牛執行了1300,當前餘額:20200
胖子執行了800,當前餘額:21000
這裡使用了一個物件鎖,來控制對併發程式碼的訪問。不管執行多少次,執行次序如何,最終餘額均為21000,這個結果是正確的。
有關原子量的用法很簡單,關鍵是對原子量的認識,原子僅僅是保證變數操作的原子性,但整個程式還需要考慮執行緒安全的。

九、障礙器

     Java5中,新增了障礙器類,為了適應一種新的設計需求,比如一個大型的任務,常常需要分配好多子任務去執行,只有當所有子任務都執行完成時候,才能執行主任務,這時候,就可以選擇障礙器了。障礙器是多執行緒併發控制的一種手段,用法很簡單。

下面給個例子:

package MultiThread;
import java.util.concurrent.BrokenBarrierException; 
import java.util.concurrent.CyclicBarrier; 

/** 
* Java執行緒:新特徵-障礙器 
* 
*  
*/ 
public class Test { 
        public static void main(String[] args) { 
                //建立障礙器,並設定MainTask為所有定數量的執行緒都達到障礙點時候所要執行的任務(Runnable) 
                CyclicBarrier cb = new CyclicBarrier(7, new MainTask()); 
                new SubTask("A", cb).start(); 
                new SubTask("B", cb).start(); 
                new SubTask("C", cb).start(); 
                new SubTask("D", cb).start(); 
                new SubTask("E", cb).start(); 
                new SubTask("F", cb).start(); 
                new SubTask("G", cb).start(); 
        } 
} 

/** 
* 主任務 
*/ 
class MainTask implements Runnable { 
        public void run() { 
                System.out.println(">>>>主任務執行了!<<<<"); 
        } 
} 

/** 
* 子任務 
*/ 
class SubTask extends Thread { 
        private String name; 
        private CyclicBarrier cb; 

        SubTask(String name, CyclicBarrier cb) { 
                this.name = name; 
                this.cb = cb; 
        } 

        public void run() { 
                System.out.println("[子任務" + name + "]開始執行了!"); 
                for (int i = 0; i < 999999; i++) ;    //模擬耗時的任務 
                System.out.println("[子任務" + name + "]開始執行完成了,並通知障礙器已經完成!"); 
                try { 
                        //通知障礙器已經完成,讓出鎖(並使得,跳躍的障礙數目-1)
                        cb.await(); 
                } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                } catch (BrokenBarrierException e) { 
                        e.printStackTrace(); 
                } 
        } 
}

執行結果:

[子任務B]開始執行了!
[子任務E]開始執行了!
[子任務C]開始執行了!
[子任務D]開始執行了!
[子任務A]開始執行了!
[子任務E]開始執行完成了,並通知障礙器已經完成!
[子任務B]開始執行完成了,並通知障礙器已經完成!
[子任務A]開始執行完成了,並通知障礙器已經完成!
[子任務C]開始執行完成了,並通知障礙器已經完成!
[子任務D]開始執行完成了,並通知障礙器已經完成!
[子任務F]開始執行了!
[子任務F]開始執行完成了,並通知障礙器已經完成!
[子任務G]開始執行了!
[子任務G]開始執行完成了,並通知障礙器已經完成!
>>>>主任務執行了!<<<<

從執行結果可以看出,所有子任務完成的時候,主任務執行了,達到了控制的目標

總結:

Java執行緒是Java語言中一個非常重要的部分,Java5之前,多執行緒的語言支援還是比較弱的,內容也較少,寫一個複雜的多執行緒程式是相當有挑戰性的。
在Java5以後,Java對多執行緒做了很多擴充套件,擴充套件部分稱之為併發包。這部分內容大大增強了Java多執行緒程式設計的能力,透過使用Java5執行緒新特徵的API,可以很容易的做出複雜的多執行緒程式。與其他語言相比,已經是相當強悍了。
 

相關文章