併發學習筆記(2)

避免代碼塊受到併發訪問的干擾

java語言提供了兩種機制實現這種功能java

  • Synchonized 關鍵字(調用對象內部的鎖)
    synchronized關鍵字自動提供一個鎖以及相關的條件
  • 引入了ReentrantLock類。(顯示鎖)
  • 更好: JUC框架爲這些基礎機制提供了獨立的類: 線程池,或者高級一點專門作併發的工具的支持

ReentrantLock類 - 鎖

Lock 與synchronized 區別

Lock 不是Java語言內置(compared to synchronized),Lock是一個類,經過這個類能夠實現同步訪問;安全

Lock 和 synchronized有一點很是大的不一樣,採用synchronized不須要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完以後,系統會自動讓現場釋放對鎖的佔用,而Lock必需要用戶手動釋放,若是沒有主動釋放鎖,將會產生死鎖。 + Lock優缺點多線程

Lock優缺點(compared to Synchronized)

Lock 能完成synchronized所實現的全部功能,並且比synchronized更好的性能。並且沒有synchronized簡潔。
可是:
1. 若是但願當獲取鎖時,有一個等待時間,不會無限期等待下去。
2. 但願當獲取不到鎖時,可以響應中斷
3. 當讀多,寫少的應用時,但願提升性能
4. 獲取不到鎖時,當即返回 false。獲取到鎖時返回 true。併發

用ReentrantLock 保護代碼塊的基本結構以下:框架

myLock.lock();
try{
   critical section
}
finally{
    myLock.unlock(); // 把解鎖語句放在finally子句內是相當重要的。若是在臨界區的代碼拋異常,鎖必須被釋放。不然,其餘線程將永遠阻塞。
}

用鎖來保護Bank類的transfer方法函數

public class Bank
{
    private Lock bankLock = new ReentrantLock();
    public void transfer(int from, int to, int amount){
        bankLock.lock();
        try{
            accounts[from] -= amount;
            account[to] += amount; 
        }
        finally{
            bankLock.unlock();
        }
    }
}

這個結構確保任什麼時候刻只有一個線程進入臨界區,一旦一個線程封鎖了鎖對象,其餘任何線程都沒法經過lock語句。當其餘線程調用lock時,它們被阻塞,直到第一個線程釋放鎖對象工具

JUC包關於Lock
java.util.concurrent.locks.Lock
- void lock()
獲取這個鎖;若是鎖同時被另外一個線程擁有則發生阻塞。
- void unlock()
釋放這個鎖
java.util.concurrent.locks.ReentrantLock
- ReentrantLock()
構建一個能夠被用來保護臨界區的可重入鎖
- ReentrantLock(boolean fair)
構建一個帶有公平策略的鎖。一個公平鎖偏心等待時間最長的鎖,可是公平的保證會致使大大下降性能。性能

條件對象

一般線程進入臨界區,卻發如今某一條件知足以後它才能執行。要使用一個條件對象來管理那些已經得到了一個鎖可是不能作擁有工做的線程。
好比銀行的模擬程序。咱們避免沒有足夠資金的帳戶做爲轉出帳戶. 以下的代碼是不能夠的,代碼有可能在transfer方法以前被中斷,在線程在此運行前,帳戶餘額可能已經低於提款金額了。this

javaif(bank.getBalance(from) >= amount)
    // thread might be deactivated at this point
    bank.transfer(from,to,amount);

因此必須確保沒有其餘線程再檢查餘額和轉帳活動之間修改金額。經過鎖來保護檢查與轉帳動做的原子性,來作到這一點:線程

javapublic void transfer(int from, int to, int amount){
    backLock.lock();
    try{
        while(accounts[from] < amount){
            // wait
        }
        // transfer funds;
    }
    finally{
        bankLock.unlock();
    }
}

當帳戶沒有足夠的餘額的時候,應該作什麼?當前線程陷入wait until 另外一個線程向帳戶注入了資金。可是鎖的排他性致使其餘線程沒有進行存款操做的機會。這就是爲何須要調節對象的緣由。

一個鎖對象能夠有一個或者多個相關的條件對象。能夠用newCondition方法得到一個條件對象。

javaclass Bank{
    private Condition sufficientFunds;
    public Bank(){
        sufficientFunds = bankLock.newCondition();
    }
 }

若是transfer方法發現餘額不足,就能夠調用sufficientFunds.await() 當前線程被阻塞,並放棄了鎖;一旦一個線程調用了await(),它進入了該條件的等待集(進入等待狀態)。當鎖可用時,該線程不能立刻解除阻塞,相反,它仍然處於阻塞狀態,也就是本身不能激活本身,須要另外一個線程調用同一條件的signalAll方法爲止。而signalAll方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢查該條件。
因此正確的代碼是:

javapublic void transfer(int from, int to, int amount){
    backLock.lock();
    try{
        while(accounts[from] < amount)
           sufficientFunds.await();
        // transfer funds;
        ...
       sufficientFunds.signalAll();
    }
    finally{
        bankLock.unlock();
    }
}
  • Condition newCondition()
    返回一個與該鎖相關的條件對象。
    java.util.concurrent.locks.Condition
  • void await()
    將該線程放到條件的等待集中
  • void signalAll()
    解除該條件的等待集中的全部線程的阻塞狀態
  • void signal()
    從該條件的等待集中隨機地選擇一個線程,解除其阻塞狀態

總結一下有關鎖(外部鎖)和條件的關鍵之處:
- 鎖用來保護代碼片斷,任什麼時候刻只能有一個線程執行被保護的代碼
- 鎖能夠管理試圖進入被保護代碼段的線程
- 鎖能夠擁有一個或多個相關的條件對象
- 每一個條件對象管理那些已經進入被保護代碼段但還不能運行的先。


Java Synchronized keywords

synchronized 是java的關鍵字,也就是說是java語言內置的特性, 是託管給JVM執行的。
java的每個對象都有一個內部鎖。若是一個方法用synchronized聲明,那麼對象的鎖將保護整個方法。namely,要調用該方法線程必須得到內部的對象鎖。經過使用synchonized 塊能夠避免競爭條件;synchonized 修飾的同步代碼塊確保了一次只能一個線程執行同步的代碼塊。全部其它試圖進入同步塊的線程都會阻塞,直到同步塊裏面的線程退出這個塊。

用Synchronized保護代碼塊的基本結構以下:

public synchronized void method(){
...
}

synchronized鎖定的是調用這個同步方法的對象。 namely 當一個對象P1在不一樣的線程中執行這個同步方法時,不一樣的線程會造成互斥,達到同步的效果。可是這個對象所屬的類的另外一個對象P2卻能調用這個被加了synchonized的方法。
上述代碼等同於

public void method(){
synchronized(this){...}
}

this 指的是調用這個方法的對象,可見同步方法實質上是將synchronized做用於object reference -- 拿到了P1對象鎖的線程,才能調用調用P1的同步方法。

javaclass Bank{
    private double[] accounts;
    public synchronized void tranfer(int from, int to, int amount) throws InterruptedException{
        while(accounts[from] < amount)
            wait();// wait on intrinsic object lock's single condition
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll(); // notify all threads waiting on the condition
    }
}

能夠看到synchronized關鍵字來編寫代碼要簡潔的多。要理解這一代碼,再一次重申:每個對象有一個內部鎖,而且該鎖有一個內部條件。 由內部鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程
java的 synchronized 關鍵字可以做爲函數的修飾符,也可做爲函數內的語句,也就是平時說的同步方法和同步語句塊.

而不管 synchronized 關鍵字是加在了方法上仍是對象上,他取得的鎖都是對象,而不是把一段代碼或者函數當作鎖; 每一個對象只有一個鎖(lock)與之關聯。 實現同步是要很大的系統開銷做爲代價的,甚至可能形成死鎖,因此要儘可能難免無謂的同步控制。

相關條件

內部對象鎖只有一個相關條件。wait方法添加一個線程到等待集中,notifyAll/notify方法解除等待線程的阻塞狀態。調用wait or notifyall等價於

intrinsicCondition.await()
intrinsicCondition.signalAll()
public synchronized void transfer(int from, int to, double amount) throws InterruptedException{
        while(accounts[from] < amount){
            wait();
        }
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll();
    }

java.lang.Object
- void notifyAll()
解除那些在該對象上調用wait方法的線程的阻塞狀態
java.util.concurrent.locks.Condition
- void await()
將該線程放到條件的等待集中
- void signalAll()
解除該條件的等待集中的全部線程的阻塞狀態
- void signal()
從該條件的等待集中隨機地選擇一個線程,解除其阻塞狀態

synchorinzed(缺陷)

- 不能中斷一個正在試圖得到鎖的線程;

若是一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其餘線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這裏獲取鎖的線程釋放鎖只會有兩種狀況:

  1)獲取鎖的線程執行完了該代碼塊,而後線程釋放對鎖的佔有;
  2)線程執行發生異常,此時 JVM 會讓線程自動釋放鎖。

  那麼若是這個獲取鎖的線程因爲要等待 IO 或者其餘緣由(好比調用sleep方法)被阻塞了,可是又沒有釋放鎖,其餘線程便只能乾巴巴地等待,試想一下,這多麼影響程序執行效率。

- 試圖得到鎖時不能設定超時;

- 每一個鎖僅有單一的條件,可能不夠的;

總結

在代碼中應該使用哪種呢? Lock 和 Condition對象仍是同步方法?
下面是Core java的一些建議:

  • 最好既不是用Lock/Condition 也不是用synchonized關鍵字。 在許多狀況下可使用JUC包中的一種機制,它會爲你處理全部加鎖.
  • 若是synchronized關鍵字適合你的程序,那麼儘可能使用它,這樣能夠減小編寫的代碼數量,減小出錯概率。
  • 若是特別須要Lock/Condition結果提供的獨有特性,才使用Lock/Condition

5.18

阻塞隊列

對於許多線程問題,能夠經過使用一個或多個隊列以優雅且安全的方式將其形式化。好比生產者線程向隊列插入元素,消費者線程則取出它們。使用隊列,能夠安全地從一個線程向另外一個線程傳遞數據。

阻塞隊列是一種比lock, synchonized更高級的管理同步的方法。

具體實現:
先定義一個BlockingQueue,隊列放共享的資源,而後多個線程取或者存而後直接調用他的函數放入和取出元素就好了.

相關文章
相關標籤/搜索