文章出處

并發編程這方面以前關注得比較少,惡補一下,推薦一個好的網站:并發編程網 - ifeve.com,上面全是各種大牛原創或編譯的并發編程文章。

今天先來學習Semaphore(信號量),字面上看,根本不知道這東西是干啥的,借用 并發工具類(三)控制并發線程數的Semaphore一文中的交通紅綠信號燈的例子來理解一下:

一條4車道的主干道,假設100米長,每輛車假設占用的長度為10米(考慮到前后車距),也就是說這條道上滿負載運行的話,最多只能容納4*(100/10)=40輛車,如果有120輛車要通過的話(為簡單起見,一波40輛,分成3波),就必須要紅綠信號燈來調度了,對于最前面的一波來講,它們看到的是綠燈,允許通過,第一波全進入道路后,紅綠燈變成紅色,表示后面的2波,要停下來等候第1波車輛全通過,然后紅綠燈才會變成綠色,讓第2波通過,如此運轉下去....

 

這跟多線程并發有啥關系呢?Semaphore就是紅綠信號燈,3波車輛就是3個并發的線程,而主干道就是多個線程要并發訪問的公用資源,由于資源有限,所以必須通過Semaphore來控制線程對資源的訪問,否則就變成資源競爭,嚴重的話會導致死鎖等問題。

 

下面用一個示例演示,假設有N個并發線程都要打印文件,但是打印機只有1臺,先來一個打印隊列類:

package yjmyzz.lesson01;

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)

package yjmyzz.lesson01;

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());
    }
}

好了,測試一把:

package yjmyzz.lesson01;

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的部分,變成下面這樣:

package yjmyzz.lesson01;

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個人都看中了同一個箱子,都要用手中的鑰匙去搶著開箱)。

所以得改進一下:

package yjmyzz.lesson02;

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上了,出現了多個線程同時搶一個資源的情況。

 


參考文章:

http://ifeve.com/thread-synchronization-utilities-2/

http://ifeve.com/thread-synchronization-utilities-3/

http://ifeve.com/concurrency-semaphore/


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()