1. 程式人生 > >java 併發之 Semphore

java 併發之 Semphore

簡介

Semaphore(訊號量)是用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。很多年以來,我都覺得從字面上很難理解Semaphore所表達的含義,只能把它比作是控制流量的紅綠燈,比如XX馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入XX馬路,但是如果前一百輛中有五輛車已經離開了XX馬路,那麼後面就允許有5輛車駛入馬路,這個例子裡說的車就是執行緒,駛入馬路就表示執行緒在執行,離開馬路就表示執行緒執行完成,看見紅燈就表示執行緒被阻塞,不能執行。

示例

下面用一個示例演示,假設有N個併發執行緒都要列印檔案,但是印表機只有1臺,先來一個列印佇列類:

import java.util.concurrent.Semaphore;
public class PrintQueue {
    private final Semaphore semaphore;
    public PrintQueue() {
        semaphore = new Semaphore(1);//限定了共享資源只能有1個(相當於只有一把鑰匙)
    }

    public void printJob(Object document) {
        try
{ semaphore.acquire();//取得對共享資源的訪問權(即拿到了鑰匙)) long duration = (long) (1 + Math.random() * 10); System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } finally
{ semaphore.release();//鑰匙用完了,要還回去,這樣其它執行緒才能繼續有序的拿到鑰匙,訪問資源 } } }

由於是在多執行緒環境中,真正執行的作業處理,得繼承自Runnable(或Callable)


public class Job implements Runnable {

    private PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob(new Object());
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}

好了,測試一把:


public class Main {
    public static void main(String args[]) {

        PrintQueue printQueue = new PrintQueue();

        int threadCount = 3;

        Thread thread[] = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            thread[i] = new Thread(new Job(printQueue), "Thread" + i);
        }

        for (int i = 0; i < threadCount; i++) {
            thread[i].start();
        }
    }
}
輸出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job during 7 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job during 5 seconds
Thread2: The document has been printed
Thread1: PrintQueue: Printing a Job during 1 seconds
Thread1: The document has been printed

從輸出上看,執行緒0列印完成後,執行緒2才開始列印,然後才是執行緒1,沒有出現一哄而上,搶佔印表機的情況。這樣可能沒啥感覺,我們把PrintQueue如果去掉Semaphore的部分,變成下面這樣:


public class PrintQueue {

   //private final Semaphore semaphore;

   public PrintQueue() {
       //semaphore = new Semaphore(1);//限定了共享資源只能有1個(相當於只有一把鑰匙)
   }

   public void printJob(Object document) {
       try {
           //semaphore.acquire();//取得對共享資源的訪問權(即拿到了鑰匙))
           long duration = (long) (1 + Math.random() * 10);
           System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
           Thread.sleep(duration);
       } catch (InterruptedException e) {
           e.printStackTrace();
       } finally {
           //semaphore.release();//鑰匙用完了,要還回去,這樣其它執行緒才能繼續有序的拿到鑰匙,訪問資源
       }
   }
}
這回的輸出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread2: PrintQueue: Printing a Job during 4 seconds
Thread1: PrintQueue: Printing a Job during 8 seconds
Thread0: PrintQueue: Printing a Job during 1 seconds
Thread0: The document has been printed
Thread2: The document has been printed
Thread1: The document has been printed

可以發現,3個執行緒全都一擁而上,同時開始列印,也不管印表機是否空閒,實際應用中,這樣必然出問題。

好的,繼續,突然有一天,公司有錢了,又買了2臺印表機,這樣就有3臺印表機了,這時候怎麼辦呢?簡單的把PrintQueue構造器中的

public PrintQueue() {
    semaphore = new Semaphore(3);
}

就行了嗎?仔細想想,就會發現問題,程式碼中並沒有哪裡能告訴執行緒哪個印表機正在列印,哪個印表機當前空閒,所以仍然有可能出現N個執行緒(N<=3)同時搶一臺印表機的情況(即:如果把控制權當成鑰匙的話,相當於有可能3個人各領取到了1把鑰匙,但是這3把鑰匙是相同的,3個人都看中了同一個箱子,都要用手中的鑰匙去搶著開箱)。

所以得改進一下:

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PrintQueue {

    private boolean freePrinters[];//用來存放印表機的狀態,true表示空閒,false表示正在列印

    private Lock lockPrinters;//增加了鎖,保證多個執行緒,只能獲取得鎖,才能查詢哪臺印表機空閒的

    private final Semaphore semaphore;


    public PrintQueue() {
        int printerNum = 3;//假設有3臺印表機
        semaphore = new Semaphore(printerNum);
        freePrinters = new boolean[printerNum];

        for (int i = 0; i < printerNum; i++) {
            freePrinters[i] = true;//初始化時,預設所有印表機都空閒
        }
        lockPrinters = new ReentrantLock();
    }


    private int getPrinter() {
        int ret = -1;
        try {
            lockPrinters.lock();//先加鎖,保證1次只能有1個執行緒來獲取空閒的印表機
            for (int i = 0; i < freePrinters.length; i++) {
                //遍歷所有印表機的狀態,發現有第1個空閒的印表機後,領取號碼,
                // 並設定該印表機為繁忙狀態(因為馬上就要用它)
                if (freePrinters[i]) {
                    ret = i;
                    freePrinters[i] = false;
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最後別忘記了解鎖,這樣後面的執行緒才能上來領號
            lockPrinters.unlock();
        }
        return ret;
    }

    public void printJob(Object document) {
        try {
            semaphore.acquire();

            int assignedPrinter = getPrinter();//領號
            long duration = (long) (1 + Math.random() * 10);
            System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n", Thread.currentThread().getName(),
                    assignedPrinter, duration);
            Thread.sleep(duration);
            freePrinters[assignedPrinter] = true;//列印完以後,將該印表機重新恢復為空閒狀態

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}
測試一下,這回把執行緒數增加到5,輸出結果類似下面這樣:

Thread0: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread2: The document has been printed
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread3: The document has been printed
Thread1: The document has been printed

從輸出結果可以看出,一次最多隻能有3個執行緒使用這3臺印表機,而且每個執行緒使用的印表機互不衝突,列印完成後,空閒的印表機會給其它執行緒繼續使用,繼續折騰,如果把getPrinter()中加鎖的部分去掉,即:

private int getPrinter() {
    int ret = -1;
    try {
        //lockPrinters.lock();//先加鎖,保證1次只能有1個執行緒來獲取空閒的印表機
        for (int i = 0; i < freePrinters.length; i++) {
            //遍歷所有印表機的狀態,發現有第1個空閒的印表機後,領取號碼,
            // 並設定該印表機為繁忙狀態(因為馬上就要用它)
            if (freePrinters[i]) {
                ret = i;
                freePrinters[i] = false;
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        //最後別忘記了解鎖,這樣後面的執行緒才能上來領號
        //lockPrinters.unlock();
    }
    return ret;
}

再跑一下,結果如何,為了放大沖突,這回開到15個執行緒來搶3臺印表機,輸出如下:

Thread0: Going to print a job
Thread14: Going to print a job
Thread13: Going to print a job
Thread12: Going to print a job
Thread11: Going to print a job
Thread10: Going to print a job
Thread9: Going to print a job
Thread8: Going to print a job
Thread7: Going to print a job
Thread6: Going to print a job
Thread5: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
Thread0: The document has been printed
Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
Thread13: The document has been printed
Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
Thread11: The document has been printed
Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
Thread14: The document has been printed
Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread12: The document has been printed
Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
Thread10: The document has been printed
Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
Thread8: The document has been printed
Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
Thread7: The document has been printed
Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
Thread9: The document has been printed
Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
Thread5: The document has been printed
Thread6: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
Thread1: The document has been printed
Thread3: The document has been printed
Thread2: The document has been printed
注意標粗的部分:Thread0與Thread14同時分配到了Printer0上了,出現了多個執行緒同時搶一個資源的情況。