Java併發:synchronized

                                     Java併發:synchronized的使用


一.線程安全  

        在單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現同時訪問同一個資源的情況,這種資源可以是各種類型的的資源:一個變量、一個對象、一個文件、           一個數據庫表等,而當多個線程同時訪問同一個資源的時候,就會存在一個問題:

  由於每個線程執行的過程是不可控的,所以很可能導致最終的結果與實際上的願望相違背或者直接導致程序出錯。
  
  假如兩個線程分別用thread-1和thread-2表示,某一時刻,thread-1和thread-2都讀取到了數據X,那麼可能會發生這種情況:
  thread-1去檢查數據庫中是否存在數據X,然後thread-2也接着去檢查數據庫中是否存在數據X。
  結果兩個線程檢查的結果都是數據庫中不存在數據X,那麼兩個線程都分別將數據X插入數據庫表當中。
  這個就是線程安全問題,即多個線程同時訪問一個資源時,會導致程序運行結果並不是想看到的結果。
  這裏面,這個資源被稱爲:臨界資源(也有稱爲共享資源)。
  也就是說,當多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。
  不過,當多個線程執行一個方法,方法內部的局部變量並不是臨界資源,因爲方法是在棧上執行的,而Java棧是線程私有的,因此不會產生線程安全問題。

二.解決線程安全

    那麼一般來說,是如何解決線程安全問題的呢?

  基本上所有的併發模式在解決線程安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
  通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。
  在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
  本文主要講述synchronized的使用方法。

三.synchronized同步方法或者同步塊  

        在瞭解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。

  舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其他線程便只能等待。
  在Java中,每一個對象都擁有一個鎖標記(monitor),也稱爲監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
  在Java中,可以使用synchronized關鍵字來標記一個方法或者代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程纔會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。
  下面通過幾個簡單的例子來說明synchronized關鍵字的使用:
  1.synchronized方法
  下面這段代碼中兩個線程分別調用insertData對象插入數據:

public class Test {
 
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
         
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
    }  
}
 
class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
     
    public void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入數據"+i);
            arrayList.add(i);
        }
    }
}
  此時程序的輸出結果爲:
  
  說明兩個線程在同時執行insert方法。
  而如果在insert方法前面加上關鍵字synchronized的話,運行結果爲:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
     
    public synchronized void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入數據"+i);
            arrayList.add(i);
        }
    }
}
 

 
  從上輸出結果說明,Thread-1插入數據是等Thread-0插入完數據之後才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
  這就是synchronized方法。
  不過有幾點需要注意:
  1)當一個線程正在訪問一個對象的synchronized方法,那麼其他線程不能訪問該對象的其他synchronized方法。這個原因很簡單,因爲一個對象只有一把鎖,當一個線程獲取了該對象的鎖之後,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
  2)當一個線程正在訪問一個對象的synchronized方法,那麼其他線程能訪問該對象的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該對象的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他線程是可以訪問這個方法的,
  3)如果一個線程A需要訪問對象object1的synchronized方法fun1,另外一個線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型),也不會產生線程安全問題,因爲他們訪問的是不同的對象,所以不存在互斥問題。
  2.synchronized代碼塊
  synchronized代碼塊類似於以下這種形式:
synchronized(synObject) {
         
    }
  當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。
  synObject可以是this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。如果是獲取該屬性的鎖,那麼這個對象的其他synchronized方法即便這個對象被鎖了也可以被其他線程執行。操作會更細粒度一些。也就是說如果這個對象有多個synchronized的話,用屬性鎖會效率更高,不會影響其他的synchronized方法。如果這對象只有一個synchronized方法,那用對象鎖也就無所謂了。
  比如上面的insert方法可以改成以下兩種形式:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
     
    public void insert(Thread thread){
        synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入數據"+i);
                arrayList.add(i);
            }
        }
    }
}

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();
     
    public void insert(Thread thread){
        synchronized (object) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入數據"+i);
                arrayList.add(i);
            }
        }
    }
}


  從上面可以看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。因爲也許一個方法中只有一部分代碼只需要同步,如果此時對整個方法用synchronized進行同步,會影響程序執行效率。而使用synchronized代碼塊就可以避免這個問題,synchronized代碼塊可以實現只對需要同步的地方進行同步。
  另外,每個類也會有一個鎖,它可以用來控制對static數據成員的併發訪問。
       在Java中,每一個類也都擁有一個鎖標記(monitor),也稱爲監視器,多線程同時訪問類的某個static成員變量時,線程只有獲取了該類的鎖才能訪問。
  並且如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因爲訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是對象鎖,所以不存在互斥現象。
看下面這段代碼就明白了:

public class Test {
 
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}
 
class InsertData { 
    public synchronized void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }
     
    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}


  執行結果;
  
  第一個線程裏面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。
  下面我們看一下synchronized關鍵字到底做了什麼事情,我們來反編譯它的字節碼看一下,下面這段代碼反編譯後的字節碼爲:
public class InsertData {
    private Object object = new Object();
     
    public void insert(Thread thread){
        synchronized (object) {
         
        }
    }
     
    public synchronized void insert1(Thread thread){
         
    }
     
    public void insert2(Thread thread){
         
    }
}
  
  從反編譯獲得的字節碼可以看出,synchronized代碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執行時會讓對象的鎖計數加1,而monitorexit指令執行時會讓對象的鎖計數減1,其實這個與操作系統裏面的PV操作很像,操作系統裏面的PV操作就是用來控制多個線程對臨界資源的訪問。對於synchronized方法,執行中的線程識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設置,然後它自動獲取對象的鎖,調用方法,最後釋放鎖。如果有異常發生,線程自動釋放鎖。
  
  有一點要注意:對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,因此不會由於異常導致出現死鎖現象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章