Java(8-3 01)多線程同步

上一次,我們說了關於同步和條件對象的使用原因和使用方法,這一次,我們接着上回的問題來說,這一節,我們將會討論關於synchronized關鍵字的作用和用法,同步阻塞的用法,監視器的概念,以及Volatile域的作用,使用原因和用法。

Part 1 synchronized關鍵字

先回顧下上一節,我們知道了鎖對象的使用,以及在線程進入鎖對象後,如何用條件對象處理那些暫時還不能運行的代碼。Java的這種機制爲我們提供了很大的操作空間,但是實際情況中並不需要這樣控制。來看看這個吧,從Java1.0版本開始,Java中的每一個對象都有一個內部鎖。如果一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法!

也就是說每個對象都有個鎖,聲明方法前加個synchronized將會把這個對象的鎖用來保護這個方法,這樣我們就可以實現之前說的鎖對象功能了 。

那條件對象的問題如何解決呢? 在內部對象鎖的機制中,他將會並且只有一個相關條件(這就是就侷限啦,之前的方法,我們可以設置很多條件對象),而且synchronized中的條件調用的並不是signaAll()
和await()方法,而是wait()和notify()方法

所以,我們之前的代碼可以修改一下:

class Bank(){
    private double[] accounts;

    public synchronized void transfer(int form, int to, int amount)throws InterruptedException
    {
        while(accounts[from] < amount)
            wait();  // 注意這裏和我們條件對象的區別,調用的不是await()方法而是wait()方法
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll(); // 這裏相當於我們條件對象的signalAll()方法
    }

    public synchronized double getTotalBalance(){...}
}

(加粗)可以看到使用synchronized關鍵字來編寫代碼要簡潔地多。我們要理解,每一個對象都有一個內部鎖,並且該鎖有一個內部條件;由鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程。

將靜態方法設置爲synchronized也是合法的。如果調用這種方法,該方法將會獲得相關類對象的內部鎖。

內部鎖和條件雖然簡便,易於閱讀,但是也存在一個缺陷:
(1)不能中斷一個正在試圖獲得鎖的線程。
(2)試圖獲得鎖時不能設定超時。
(3)每個鎖僅有單一的條件,可能是不夠的。

那我們應該如何選擇使用那種機制好呢?最好Lock/Condition和synchronized都別使用,在許多情況下,我們可以使用java.util.concurrent包中的一種機制,它會爲你處理所有的加鎖,例如如何使用阻塞隊列來同步完成一個共同任務的線程。還應該看一些並行流的知識(二卷一章節)。
但是我們仍然提倡他,如果synchronized適合你的程序,請儘量使用它,這樣可以減少編寫的代碼數量,減少出錯的機率。

Part 2 同步阻塞(學長說的那種方法)

正如我們剛剛討論的那樣,線程可以通過上面說的那兩種方法實現同步,這裏還有一種方法可以獲得鎖,通過進入一個同步阻塞。當線程表現如下形式的阻塞時:

public class Bank()
{
    private double[] acounts;
    private Object lock = new Object();
    ...
    public void transfer(int from, int to, int amount)
    {
        synchronized(lock) //將Object對象上鎖
        {
            accounts[from] -= amount;
            accounts[to] += amount;
        }
        ...
    }
}

在這裏Object的 lock對象的創建僅僅是用來使用每個Java對象持有的鎖。

有時候程序員們使用一個對象的鎖來實現額外的原子操作,實際上稱爲客戶端鎖定!
我們在這裏舉個例子,例如考慮Vector類,一個列表,它的方法是同步的。現在,假定在Vector中存儲銀行餘額。這裏有一個transfer方法的原始實現:
public void transfer(Vector accounts, int from,int to,int amount) // Error
{
accounts.set(from,accounts.get(from) - amount);
accounts.set(to,accounts.get(to) + amount);

}
Vector類的get和set方法是同步的,但是,這對於我們並沒什麼幫助。在第一次對get的調用一斤完成以後,一個線程完全可能在transfer方法中被剝奪運行權。於是,同一個存儲位置,可能會被存入不同的值。但是我們可以截獲這個鎖:

pubilc void transfer(Vector accounts,int form,int to,int amount)
{
synchronized(accounts)
{
accounts.set(from,accounts.get(from) - amount);
accounts.set(to,accounts.get(to) + amount);
}

}
這個方法可以工作,但是他完全依賴與一個事實,Vector類對自己的所有可修改的方法都使用了內部鎖(就是對與Vector類內部的各種操作也用了鎖,否則很可能Vector類內部操作時,產生內存數據被擦除的情況)。然而,這是真的嗎?Java並沒給出承諾,必須仔細閱讀源碼纔能有答案,所以說這種客戶端鎖定是非常脆弱的,使用時候要非常小心(尤其是對各種其他數據結構進行操作的時候)

Part 3 關於Volatile:

Volatile真正解決的問題是 JVM 在-server模式下(注意普通運行模式下沒有此問題), 線程優先取用自己的線程私有stack中的變量值, 而不是公共堆中的值, 造成變量值老舊的問題.

換句話說, Volatile強制要求了所有線程在使用變量的時候要去公共內存堆中獲取值, 不可以偷懶使用自己的.

Volatile絕對不保證原子性操作!
具體問題非常複雜! 請參考另一位朋友的博文 轉自http://www.cnblogs.com/dolphin0520/p/3920373.html[http://www.cnblogs.com/dolphin0520/p/3920373.html]

Part4 關於監視器

用Java的術語來說 監視器是:(1)只包含私有域的類;(2)每個監視器的類對象都有一個相關的鎖;(3)使用該鎖對所有的方法進行加鎖;(4)該鎖可以有任意多個相關條件。

然而,Java對象不同於監視器,從而使得線程的安全性下降:
(1)域不要求必須是private
(2)方法不要求必須是synchronized
(3)內部鎖對客戶是可用的。
這些都是很大的安全漏洞了,引起了大神Per Brinch Hansen的嚴厲批評 !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章