Java併發編程-互斥(四)

原文:http://blog.sina.com.cn/s/blog_4b6047bc010009d4.html

原子動作
    前文講到,不同線程的操作在訪問共享數據時,會因爲交織進行而導致線程干擾和內存一致性錯誤。大多數Java語句在編譯成僞代碼後都由多條虛擬機指令組成,這使它們有可能被其他線程的語句所分割交織。不能分割交織的操作乘稱作原子動作,這些動作一旦發生,便不能在中途停止,要麼完全發生,要麼根本不發生,直至動作結束。前文所提到的++操作不是一個原子動作。雖然大部分Java語句都不是原子動作,但是也有一些動作可以認定爲是原子性的:
1.引用類型變量值的讀和寫。注意這兒是引用值的讀寫,而不是所引用對象內容的讀和寫。
2.除了long和double之外的簡單類型的讀和寫。
3.所有聲明爲volatile的變量的讀和寫,包括long和double類型以及引用類型
    原子動作是不能被交織分割的,因此可以放心使用,不用擔心線程干擾問題。但注意內存一致性錯誤對於原子動作仍然是存在的。使用volatile關鍵字能夠減小內存一致性錯誤發生的風險,任何對volatile變量的寫操作和之後進行的讀操作都會自動建立“發生過”關係。這意味着任何對於volatile變量的改變都是對其他線程可見的。另外當某線程讀一個volatile變量時,它看到的不僅僅是對該變量的最新改動,也能看到這一改變帶來的副作用。

    使用原子變量訪問要比使用互斥代碼訪問要高效得多,但是需要程序員人爲地避免內存一致性錯誤發生。是否需要額外措施避免這些錯誤往往取決於程序的規模和複雜度。java.util.concurrent包中的類提供了不依賴於互斥原語的方法,在後面的文章我們將逐步介紹。

內部鎖與互斥
    前面提到除少數原子動作能同時避免線程干擾和內存一致性錯誤外,其它操作都是需要互斥保護才能避免錯誤的發生。這些保護技術在Java語言中通過互斥方法和互斥代碼實現。
    互斥訪問機制是建立在內部鎖的實體概念上的。API規範通常稱這種實體爲“管程(monitor)”。內部鎖在這兩個問題的解決上扮演着重要的角色,它爲線程對對象的狀態進行強制排他性訪問,並建立對於可視性至關重要的“發生過”關係。
    每個對象都有一個內部鎖與其對應。如果一個線程需要排他一致性訪問對象的字段,它首先要在訪問之前獲得該對象的內部鎖。當訪問完成時需要釋放該內部鎖。線程在獲得該鎖和釋放該鎖期間稱作擁有該鎖。一旦線程擁有內部鎖,其他任何線程都不能再獲得該鎖,它們在獲得該鎖時會被阻塞。
    當線程釋放該內部鎖時,“發生過”關係就在該動作和同把鎖的後繼動作之間建立起來。
互斥語句
    創建互斥性操作的方法是互斥語句。互斥語句的語法格式如下:
synchronized(lock){
  //critical code for accessing shared data.
  //...
}

    在Java中,實現互斥語句的關鍵字叫synchronized(同步),我認爲這是一個不合適的術語。同步應該定義爲按照固定順序發生的動作序列。這兒的含義顯然是互斥訪問的含義。
    這兒lock是提供內部鎖的對象。這個語句是互斥代碼的一般寫法。另外往往整個方法需要進行互斥,這時就有所謂互斥方法。互斥方法根據方法類型的不同分爲實例互斥方法和靜態互斥方法。實例互斥方法的例子如下:
public synchronized void addName(String name){
   //Adding name to a shared list.
}

    互斥實例方法實際獲得的是當前實例對象的內部鎖,前面的這個實例方法相當於下面寫法的互斥語句:
public void addName(String name){
    synchronized(this){
        //Adding name to a shared list.
    }
}

    靜態互斥方法的例子如下:
publi class ClassA{
    public static synchronized void addName(String name){
       //Adding to a static shared list.
    }
}

    靜態互斥方法實際獲得的是當前類Class對象的內部鎖,前面這個靜態方法的相當於下面寫法的互斥語句:
public class ClassA{
    public static void addName(String name){
        synchronized(ClassA.class){
           //Adding to static shared list.
        }
    }
}

    互斥語句在互斥代碼開始時獲得對象的內部鎖,在語句結束或互斥方法返回時釋放鎖。互斥語句塊相對於互斥方法來說主要有兩個作用:
1.避免不必要的死鎖。有些被互斥代碼塊中如果包含其他互斥方法或者代碼的調用,可能會造成死鎖。
2.細化互斥的粒度。比如MsLunch有兩個實例字段c1和c2從來不一起使用。所有對這些字段的更新必須互斥進行,但沒理由防止c1和c2兩個字段更新操作的交織,這樣也會因不必要的阻塞減小兩種操作之間的併發度。可以專門爲每個字段定義一個對象鎖,而沒必要使用和this關聯的互斥實例方法:

    public class MsLunch {
        private long c1 = 0;
        private long c2 = 0;
        private Object lock1 = new Object();
        private Object lock2 = new Object();

        public void inc1() {
            synchronized(lock1) {
                c1++;
            }
        }

        public void inc2() {
            synchronized(lock2) {
                c2++;
            }
        }
    }

互斥重入
    注意線程不能獲得已經被另一線程所擁有的鎖,但線程可以獲取它已經擁有的鎖。允許線程多次獲取同一把鎖使互斥方法可以重入,這樣互斥代碼就能直接或者間接調用另外有互斥代碼的方法,而兩處互斥代碼可以使用同一把鎖。如果沒有互斥重入機制,我們需要非常小心的編碼才能避免這種調用帶來的死鎖。
補充
    注意構造函數是不能互斥的。在構造函數前使用synchronized關鍵字是語法錯誤。互斥構造函數沒有任何意義,因爲在其構造時,只有創建該對象的線程可以訪問它。在創建要被共享的對象時,一定要注意避免對象的引用提前“泄漏”。比方說想維護一個包含所有實例的靜態列表,可能會有這樣寫代碼:
public class A{
  public static ArrayList<A> instances=new ArrayList<A>();
  //...
  public A(){
     ...
     instances.add(this);
     ...
  }
}

    那麼當線程通過new A()生成A的實例時,其他線程可以通過訪問A.instances而獲得該對象,而該對象目前還沒有構建完畢,這時就會造成錯誤。

小結
    互斥方法和互斥語句爲java提供了簡單的防止線程干擾和內存一致性錯誤的辦法,如果一個對象對多個線程可見,所有對該對象的讀和寫操作都應該通過互斥代碼段或互斥方法來實現互斥性訪問。

    當然final字段的訪問是不需要互斥的。因爲一旦初始化完畢,這些字段只能進行讀操作,因此可被不同線程之間安全共享。
    這種互斥方式對於避免兩種問題非常有效,但同時也帶來了其他各種問題。其中最主要的問題就是對線程活性的影響,這些問題通常有死鎖(deadlock)、飢餓(starvation)和活瑣(livelock)。

    另外代碼互斥如果使用不恰當,如互斥粒度掌握不好,就會造成併發度的降低,從而降低整個應用程序的性能。
    這些問題將在後面的文章介紹。 

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