Java多線程之同步與阻塞隊列

多線程對共享數據的讀寫涉及到同步問題,鎖和條件是線程同步的強大工具。鎖用來保護代碼片斷(臨界區),任什麼時候刻只能有一個線程執行被保護的代碼。條件對象用來管理那些已經進入被保護的代碼段但還不能運行的線程。java

競爭條件

各線程訪問數據的次序不一樣,可能會產生不一樣的結果。下面的程序能夠實現兩個帳戶之間的轉帳,正常狀況下全部帳戶的總金額應該是不變的。git

public void transfer(int from, int to, double amount) {
    if (accounts[from] < amount) {
        return;
    }
    accounts[from] -= amount;
    accounts[to] += amount;
    System.out.printf(" Total Balance %10.2f\n", getTotalBalance());
}

可是在上面程序的運行中發現輸出的總金額是變化的,這是由於transfer()方法執行的過程當中會被中斷,可能存在幾個線程同時讀寫帳戶餘額。問題的根源在於轉帳這一系列動做不是原子操做,而且沒有使用同步。固然同步使用不當也會形成死鎖(全部線程都阻塞的狀態)。github

鎖對象

可使用鎖和條件對象實現同步數據存取。鎖可以保護臨界區,確保只有一個線程執行。編程

注意,在finally子句中不要忘記解鎖操做。若因異常拋出釋放,對象可能受損。數組

互斥鎖

ReentrantLock類可以有效防止代碼塊受併發訪問的干擾。緩存

private Lock bankLock;
private Condition sufficientFunds;
public void transfer(int from, int to, double amount) throws InterruptedException {
    bankLock.lock();
    try {
        while (accounts[from] < amount) {
            sufficientFunds.await();
        }
        accounts[from] -= amount;
        accounts[to] += amount;
        System.out.printf(" Total Balance %10.2f\n", getTotalBalance());
        sufficientFunds.signalAll();
    } finally {
        bankLock.unlock();
    }
}

每個Bank對象有本身的ReentrantLock對象,若是兩個線程試圖訪問同一個Bank對象,那麼鎖以串行方式提供服務。可是若是兩個線程訪問的是不一樣的Bank對象,兩個線程都不會發生阻塞。安全

對於全部帳戶總金額的獲取方法也須要加鎖才能保證正確執行。鎖是可重入的,也就是說同一個線程能夠重複的得到已經持有的鎖。鎖保持一個持有計數來跟蹤嵌套得到鎖的次數,當持有計數變爲0時,線程釋放鎖。數據結構

public double getTotalBalance() {
    bankLock.lock();
    try {
        double sum = 0;
        for (double a : accounts) {
            sum += a;
        }
        return sum;
    } finally{
        bankLock.unlock();
    }
}

測試鎖

tryLock()方法用於嘗試獲取鎖而沒有發生阻塞。若是未得到鎖,線程能夠當即離開,去作別的事。多線程

if(myLock.tryLock()) {
    try {
        do something
    } finally {
        myLock.unlock();
    }
} else {
    do something else
}

調用帶有超時參數的tryLock(),線程能夠在等待獲取鎖的過程當中被中斷,拋出InterruptedException異常。從而容許程序打破死鎖,相似於lockInterruptibly()併發

讀寫鎖

java.util.concurrent.locks包定義了兩個鎖類:ReentrantLock類和ReentrantReadWriteLock類。在讀多寫少(不少線程從一個數據結構讀取數據,不多線程修改其中數據)的情形中,ReentrantReadWriteLock類是十分實用的。

讀鎖,容許多個讀,排斥全部寫;寫鎖,排斥全部讀和寫。

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

條件對象

條件對象用來管理那些已經得到鎖但不能工做的線程。好比當帳戶中沒有足夠餘額時,需等待別的線程的存款操做。

一個鎖對象能夠有一個或多個相關的條件對象。當一個線程調用await()等待方法時,它將進入該條件的等待集。當一個線程轉帳完成時會調用sufficientFunds.signalAll()方法,從新激活由於sufficientFunds這一條件而等待的全部線程,使這些線程從等待集中移出,狀態變爲可運行。當一個線程處於等待集中時,只能靠其餘線程來從新激活本身。

synchronized關鍵字

使用synchronized關鍵字聲明的方法,對象的鎖將保護整個方法,其實就是隱式的使用了一個內部對象鎖。內部對象鎖只有一個條件對象,使用wait()/notifyAll()/notify()操做。

public synchronized void myMethod() {
    while (! (ok to proceed)) {
        wait();
    }
    do something
    notifyAll();
}

注意,signal()notify()都是隨機選擇一個線程,解除其阻塞狀態,可能會形成死鎖。

對於sychronized修飾的方法,顯式使用鎖對象和條件對象,形式以下。

public void myMethod() {
    this.intrinsic.lock();
    try {
        while(! (ok to proceed)) {
            condition.await();
        }
        do something
        condition.signalAll();
    } finally {
        this.intrinsic.unlock();
    }
}

爲了保證操做的原子性,能夠安全地使用AtomicInteger做爲共享計數器而無需同步,這個類提供方法incrementAndGet()decrementAndGet()完成自增自減操做。

Volatile域

使用volatile關鍵字同步讀寫的必要性:

  • 因爲寄存器或緩存的存在同一內存地址可能會取到不一樣的值;

  • 編譯器優化中假定內存中的值僅在代碼中有顯式修改指令時會改變。

volatile關鍵字爲實例域的同步訪問提供了一種免鎖機制,當被聲明爲volatile域時,編譯器和虛擬機就知道該域可能被另外一個線程併發更新。使用鎖或volatile修飾符,多個線程能夠安全地讀取一個域,但volatile不提供原子性。。另外,將域聲明爲final,也能夠保證安全的訪問這個共享域。

線程局部變量

在線程間共享變量時有風險的,可使用ThreadLocal輔助類爲各個線程提供各自的實例。好比,SimpleDateFormat類不是線程安全的,內部數據結構會被下面形式的併發訪問破壞。

public static final SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateStamp = dateFormat.format(new Date());

若是不使用synchronized或鎖等開銷較大的同步,可使用線程局部變量ThreadLocal解決變量併發訪問的問題。

public static final ThreadLocal<SimpleDateFormat> dateFormat =
    new ThreadLocal<SimpleDateFormat>() {
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
String dateStamp = dateFormat.get().format(new Date());

在一個線程中首次調用get()時,會調用initialValue()方法,此後會返回屬於當前線程的實例。

對於java.util.Random類,雖是線程安全的,但多線程共享隨機數生成器倒是低效的。可使用上面提到的ThreadLocal爲各個線程提供一個單獨的生成器,還可使用ThreadLocalRandom這個便利類。

int random = ThreadLocalRandom.current().nextInt(upperBound);

阻塞隊列

上面關於同步的實現方式是Java併發程序設計基礎的底層構建塊,在實際的編程使用中,使用較高層次的類庫會相對安全方便。對於典型的生產者和消費者問題,可使用阻塞隊列解決,這樣就不用考慮鎖和條件的問題了。

生產者線程向隊列插入元素,消費者線程從隊列取出元素。當添加時隊列已滿或取出時隊列爲空,阻塞隊列致使線程阻塞。將阻塞隊列用於線程管理工具時,主要用到put()take()方法。對於offer()poll()peek()方法不能完成時,只是給出一個錯誤提示而不會拋出異常。

java.util.concurrent包提供了幾種形式的阻塞隊列:

  • LinkedBlockingQueue:無容量限制,鏈表實現;

  • LinkedBlockingDeque:雙向隊列,鏈表實現;

  • ArrayBlockingQueue:需指定容量,可指定公平性,循環數組實現;

  • PriorityBlockingQueue:無邊界優先隊列,用堆實現。

這裏有一個用阻塞隊列控制一組線程的示例,實現的功能是搜索指定目錄及子目錄中的全部文件並找出含有查詢關鍵字的行。裏面有個小技巧,一個線程搜索完畢時向阻塞隊列填充DUMMY,讓全部線程能停下來。

相關文章
相關標籤/搜索