JVM線程資源同步及交互機制

Java程序採用多線程來支持大量併發。尤其是在多核或者多CPU系統中,多線程執行程序帶來的最明顯的問題是線程之間同步管理的資源競爭以及線程交互的問題。

JVM的線程實現及其調度方式(搶佔、協作)取決於操作系統,不在本文贅述。

線程資源同步機制

有如下程序:

int i=0;
public int getNextId(){
    return i++;
}

以上程序在JVM中執行的步驟如下:
(1) JVM在堆中給i分配一個內存存儲場所(main memory),並存儲其值爲1。
(2) 線程啓動後,自動分配一片操作數棧(working memory),當線程執行到return i++時,JVM的動作分爲以下五步:

  1. 裝載i
    向main memory發起read i指令。
    當read i執行完畢,線程會將i的值從main memory複製到working memory中。
  2. 讀取i
    從main memory中讀取i。
  3. 進行i+1操作
    由線程完成。
  4. 存儲i
    將i+1的值賦值給i,然後存儲到working memory中。
  5. 寫入i
    將i的值回寫到main memory中。

從以上步驟中不難發現,從working memory到main memory的存取是需要時間的(反過來也是);i++是由多個操作完成的(讀取 自增 存儲),如果是多線程,就會出現髒讀、誤讀等現象。

對於多線程的髒讀、誤讀等現象,JVM把對於working memory的操作分爲了use、assign、load、store、lock和unlock。
對於main memory操作分爲了read、write、lock和unlock。
不難理解lock和unlock就是鎖的使用。對此,JVM提供了synchronized關鍵字、volatile關鍵字和lock/unlock機制。

採用synchronized改造如下:

public synchronized int getNextId(){
    return i++;
}

對於lock/unlock機制,可能發生死鎖,可以看看如下代碼:

private Object a=new Object();
private Object b=new Object();
public void callAB(){
    synchronized(a){
        synchronized(b){
            //do something
        }
    }
}
public void executeAB(){
    synchronized(b){
        synchronized(a){
            //do something
        }
    }
}

volatile機制有所不同,它僅用於控制線程中對象的可見性,並不能保證在此對象上操作的原子性。就像上面的i++操作,即使把i定義爲volatile也是沒用的。但對於定義爲volatile的變量,線程不會將其從main memory 複製到work memory中,而是直接在main memory上操作,它的代價雖然低,但是不能保證原子性。

可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。 用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。
volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

線程交互機制

線程交互最典型的就是連接池。連接池中通常會有get和return兩種方法。return的時候會講連接返回到緩存列表中,並將連接數+1。而get方法在判斷可使用連接數爲0後,就進入一個等待狀態,當有連接返回到連接池時,應該通知get方法不需要等待了。JVM通過wait/notify/notifyAll來支持這種等待和喚醒的需求。

典型的代碼如下:

public Connection get(){
    synchronized(this){
        if(free>0){
            free--;
            return cacheConnections.poll();
        }
        else{
            this.wait();
        }
    }
}
public void close(Connection conn){
    synchronized(this){
        free++;
        cacheConnection.offer(conn);
        this.notifyAll();
    }
}

在Sun JDK中,object.wait()還有可能被虛假喚醒(也就是說原本只能喚醒一個人,現在喚醒了兩個人,都先後拿到了鎖,然而池中只有一根冰棒,),因此需要在此確認狀態是否變更了,這種做法稱爲double check。(具體可以看看懶漢單例模式或者生產者消費者模式
單例模式:

public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                instance = new Singleton2();
            }
        }
        return instance;
    }

變更爲:

public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

這裏解釋一下爲什麼synchronized爲什麼不寫成這樣:

public static Singleton2 getInstance(){
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        return instance;
    }

其實這是一個效率問題:是由於如果加在synchronized下面的話,這其實與方法加鎖沒什麼區別。每次運行進來,線程都會阻塞。而double check保證了在創建了新實例的時候,不會阻塞。

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