1. 程式人生 > >Java多執行緒程式設計-(2)-可重入鎖以及Synchronized的其他基本特性

Java多執行緒程式設計-(2)-可重入鎖以及Synchronized的其他基本特性

原文出自 : https://blog.csdn.net/xlgen157387/article/details/78005352

一、Synchronized鎖重入

(1)關鍵字Synchronized擁有鎖重入的功能,也就是在使用Synchronized的時候,當一個執行緒得到一個物件的鎖後,在該鎖裡執行程式碼的時候可以再次請求該物件的鎖時可以再次得到該物件的鎖。

(2)也就是說,當執行緒請求一個由其它執行緒持有的物件鎖時,該執行緒會阻塞,而當執行緒請求由自己持有的物件鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞。

(3)一個簡單的例子就是:在一個Synchronized修飾的方法或程式碼塊的內部呼叫本類的其他Synchronized修飾的方法或程式碼塊時,是永遠可以得到鎖的,示例程式碼A如下:

public class SyncDubbo {

    public synchronized void method1() {
        System.out.println("method1-----");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2-----");
        method3();
    }

    public synchronized void method3() {
        System.out.println("method3-----"
); } public static void main(String[] args) { final SyncDubbo syncDubbo = new SyncDubbo(); new Thread(new Runnable() { @Override public void run() { syncDubbo.method1(); } }).start(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

執行結果:

method1-----
method2-----
method3-----
  
  • 1
  • 2
  • 3

示例程式碼A向我們演示了,如何在一個已經被synchronized關鍵字修飾過的方法再去呼叫物件中其他被synchronized修飾的方法。

(4)那麼,為什麼要引入可重入鎖這種機制哪?

我們上一篇文章中介紹了“一個物件一把鎖,多個物件多把鎖”,可重入鎖的概念就是:自己可以獲取自己的內部鎖

假如有1個執行緒T獲得了物件A的鎖,那麼該執行緒T如果在未釋放前再次請求該物件的鎖時,如果沒有可重入鎖的機制,是不會獲取到鎖的,這樣的話就會出現死鎖的情況。

就如程式碼A體現的那樣,執行緒T在執行到method1()內部的時候,由於該執行緒已經獲取了該物件syncDubbo 的物件鎖,當執行到呼叫method2() 的時候,會再次請求該物件的物件鎖,如果沒有可重入鎖機制的話,由於該執行緒T還未釋放在剛進入method1() 時獲取的物件鎖,當執行到呼叫method2() 的時候,就會出現死鎖。

(5)那麼可重入鎖到底有什麼用哪?

正如上述程式碼A和(4)中解釋那樣,最大的作用是避免死鎖。假如有一個場景:使用者名稱和密碼儲存在本地txt檔案中,則登入驗證方法和更新密碼方法都應該被加synchronized,那麼當更新密碼的時候需要驗證密碼的合法性,所以需要呼叫驗證方法,此時是可以呼叫的。

(6)關於可重入鎖的實現原理,是一個大論題,在這裡篇幅有限不再學習,有興趣可以移步至:http://www.cnblogs.com/pureEve/p/6421273.html 進行學習。

(7)可重入鎖的其他特性:父子可繼承性

可重入鎖支援在父子類繼承的環境中,示例程式碼如下:

public class SyncDubbo {

    static class Main {
        public int i = 5;
        public synchronized void operationSup() {
            i--;
            System.out.println("Main print i =" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Sub extends Main {
        public synchronized void operationSub() {
            while (i > 0) {
                i--;
                System.out.println("Sub print i = " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                Sub sub = new Sub();
                sub.operationSub();
            }
        }).start();
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

一、Synchronized的其他特性

(1)出現異常時,鎖自動釋放

就是說,當一個執行緒執行的程式碼出現異常的時候,其所持有的鎖會自動釋放,示例如下:

public class SyncException {

    private int i = 0;

    public synchronized void operation() {
        while (true) {
            i++;
            System.out.println(Thread.currentThread().getName() + " , i= " + i);
            if (i == 10) {
                Integer.parseInt("a");
            }
        }
    }

    public static void main(String[] args) {
        final SyncException se = new SyncException();
        new Thread(new Runnable() {
            public void run() {
                se.operation();
            }
        }, "t1").start();
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

執行結果如下:

t1 , i= 2
t1 , i= 3
t1 , i= 4
t1 , i= 5
t1 , i= 6
t1 , i= 7
t1 , i= 8
t1 , i= 9
t1 , i= 10
java.lang.NumberFormatException: For input string: "a"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    //其他輸出資訊
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看出,當執行程式碼報錯的時候,程式不會再執行,即釋放了鎖。

(2)將任意物件作為監視器

public class StringLock {

    private String lock = "lock";

    public void method() {
        synchronized (lock) {
            try {
                System.out.println("當前執行緒: " + Thread.currentThread().getName() + "開始");
                Thread.sleep(1000);
                System.out.println("當前執行緒: " + Thread.currentThread().getName() + "結束");
            } catch (InterruptedException e) {

            }
        }
    }

    public static void main(String[] args) {
        final StringLock stringLock = new StringLock();
        new Thread(new Runnable() {
            public void run() {
                stringLock.method();
            }
        }, "t1").start();

        new Thread(new Runnable() {
            public void run() {
                stringLock.method();
            }
        }, "t2").start();
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

執行結果:

當前執行緒: t1開始
當前執行緒: t1結束
當前執行緒: t2開始
當前執行緒: t2結束
  
  • 1
  • 2
  • 3
  • 4

(3)單例模式-雙重校驗鎖:

普通的加鎖的單例模式:

public class Singleton {

    private static Singleton instance = null; //懶漢模式
    //private static Singleton instance = new Singleton(); //餓漢模式

    private Singleton() {

    }

    public static synchronized Singleton newInstance() {
        if (null == instance) { //判斷例項是否已經被其他執行緒建立了
            instance = new Singleton();
        }
        return instance;
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

使用上述的方式可以實現多執行緒的情況下獲取到正確的例項物件,但是每次訪問newInstance()方法都會進行加鎖和解鎖操作,也就是說該鎖可能會成為系統的瓶頸,為了解決這個問題,有人提出了“雙重校驗鎖”的方式,示例程式碼如下:

public class DubbleSingleton {

    private static DubbleSingleton instance;

    public static DubbleSingleton getInstance(){
        if(instance == null){  //判斷例項是否已經被其他執行緒建立了,如果沒有則建立
            try {
                //模擬初始化物件的準備時間...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //類上加鎖,表示當前物件不可以在其他執行緒的時候建立
            synchronized (DubbleSingleton.class) { 
                //如果不加這一層判斷的話,這樣的話每一個執行緒會得到一個例項
                //而不是所有的執行緒的到的是一個例項
                if(instance == null){ //從第一次判斷是否為null到加鎖之間的時間內判斷例項是否已經被建立
                    instance = new DubbleSingleton();
                }
            }
        }
        return instance;
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

(雙重校驗鎖的方式相對於執行緒安全的懶漢模式來說,從表面上是將鎖的粒度縮小為方法內部的同步程式碼塊,而不是執行緒安全的懶漢模式同步整個方法!是鎖優化中:減小鎖粒度的一種表現形式)

但是,需要注意的是,上述的程式碼是錯誤的寫法,這是因為:指令重排優化,可能會導致初始化單例物件和將該物件地址賦值給instance欄位的順序與上面Java程式碼中書寫的順序不同。

例如:執行緒A在建立單例物件時,在構造方法被呼叫之前,就為 該物件分配了記憶體空間並將物件設定為預設值。此時執行緒A就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有完成初始化操作。執行緒B來呼叫newInstance()方法,得到的 就是未初始化完全的單例物件,這就會導致系統出現異常行為。

為了解決上述的問題,可以使用volatile關鍵字進行修飾instance欄位。volatile關鍵字在這裡的含義就是禁止指令的重排序優化(另一個作用是提供記憶體可見性),從而保證instance欄位被初始化時,單例物件已經被完全初始化。

最終程式碼如下:

public class DubbleSingleton {

    private static volatile DubbleSingleton instance;

    public static DubbleSingleton getInstance(){
        if(instance == null){
            try {
                //模擬初始化物件的準備時間...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //類上加鎖,表示當前物件不可以在其他執行緒的時候建立
            synchronized (DubbleSingleton.class) { 
                //如果不加這一層判斷的話,這樣的話每一個執行緒會得到一個例項
                //而不是所有的執行緒的到的是一個例項
                if(instance == null){ 
                    instance = new DubbleSingleton();
                }
            }
        }
        return instance;
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

那麼問題來了,為什麼volatile關鍵字可以實現禁止指令的重排序優化 以及什麼是指令重排序優化哪?

在Java記憶體模型中我們都是圍繞著原子性、有序性和可見性進行討論的。為了確保執行緒間的原子性、有序性和可見性,Java中使用了一些特殊的關鍵字申明或者是特殊的操作來告訴虛擬機器,在這個地方,要注意一下,不能隨意變動優化目標指令。關鍵字volatile就是其中之一。

指令重排序是JVM為了優化指令,提高程式執行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度(比如:將多條指定並行執行或者是調整指令的執行順序)。編譯器、處理器也遵循這樣一個目標。注意是單執行緒。可顯而知,多執行緒的情況下指令重排序就會給程式設計師帶來問題。最重要的一個問題就是程式執行的順序可能會被調整,另一個問題是對修改的屬性無法及時的通知其他執行緒,已達到所有執行緒操作該屬性的可見性。

根據編譯器的優化規則,如果不使用volatile關鍵字對變數進行修飾的,那麼這個變數被修改後,其他執行緒可能並不會被通知到,甚至在別的想愛你城中,看到變數修改順序都會是反的。一旦使用volatile關鍵字進行修飾的話,虛擬機器就會特別小心的處理這種情況,