1. 程式人生 > >Java Concurrency in Practice中物件鎖重入問題的理解

Java Concurrency in Practice中物件鎖重入問題的理解

原因:Java Concurrency in Practice 中文版21頁講解了關於物件鎖的重入的問題,一直沒有讀懂作者給的例子,今天琢磨了好久,找到了一個可以說服自己的理由……

1 原書內容如下:

當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒就會阻塞。然而,由於內建鎖是可重入的,因此如果摸個執行緒試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。“重入”意味著獲取鎖的操作的粒度是“執行緒”,而不是呼叫。重入的一種實現方法是,為每個鎖關聯一個獲取計數值和一個所有者執行緒。當計數值為0時,這個鎖就被認為是沒有被任何執行緒所持有,當執行緒請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置為1,如果同一個執行緒再次獲取這個鎖,計數值將遞增,而當執行緒退出同步程式碼塊時,計數器會相應地遞減。當計數值為0時,這個鎖將被釋放。
重入進一步提升了加鎖行為的封裝性,因此簡化了面向物件併發程式碼的開發。分析如下程式:

public class Father
{
    public synchronized void doSomething(){
        ......
    }
}

public class Child extends Father
{
    public synchronized void doSomething(){
        ......
        super.doSomething();
    }
}

子類覆寫了父類的同步方法,然後呼叫父類中的方法,此時如果沒有可重入的鎖,那麼這段程式碼件產生死鎖。由於Father和Child中的doSomething方法都是synchronized方法,因此每個doSomething方法在執行前都會獲取Child物件例項上的鎖。如果內建鎖不是可重入的,那麼在呼叫super.doSomething時將無法獲得該Child物件上的互斥鎖,因為這個鎖已經被持有,從而執行緒會永遠阻塞下去,一直在等待一個永遠也無法獲取的鎖。重入則避免了這種死鎖情況的發生。

2 我的問題如下:

說實話,讀這本書時,我本著一個學生對作者無比崇敬的心情戰戰兢兢的欣賞著,生怕自己看不懂。當我看完了作者的文字內容之後,除了認同和佩服之外,沒有產生任何的疑問。可是當我看到作者的示例程式碼是,我完全搞不懂了,心情一落千丈。我一度對作者的水平產生了懷疑,但是覺得不太可能是作者的問題,因為我深知自己的水平有多低,應該是我的問題。
懷疑如下:假設一個執行緒t1呼叫了Childl類某個例項c1的doSomething方法,那麼在成功呼叫之前t1應該先獲得c1的鎖。接下來呼叫Father(super:大家都知道,在建立c1需要先建立其父類的例項f1,super作為f1的引用)類某個例項f1的doSomething方法,那麼在成功呼叫之前t1應該先獲得f1的鎖。也就是說t1執行緒先後或得了兩個不同物件的鎖,這怎麼能叫重入呢?

3 我的探索如下:

1. 第一步探索

class _Father{
    public synchronized void dosomething(){
        System.out.println("the dosomething method of father");
    }
    public synchronized void mydosomething() throws InterruptedException{
        System.out.println("the mysomething method of father");
        Thread.sleep(3000);
    }   
}
public class _JavaConcurrency_01 extends _Father{
    public synchronized void dosomething() {
        System.out.println("the dosomething method of son");
        super.dosomething();
    }
    public void mydosomething() throws InterruptedException {
        super.mydosomething();
    }
    public static void main(String[] args) throws InterruptedException {
        final _JavaConcurrency_01 s1 = new _JavaConcurrency_01();
        // 啟動t1執行son.mydosomething方法(不需要鎖)
        // 繼續呼叫super.mydosomething方法,獲取到了f1(f1在疑問中闡述)的鎖,並讓t1續修3秒鐘
        new Thread(new Runnable() {
            public void run() {
                try {
                    s1.mydosomething();
                } catch (InterruptedException e) {
                }
            }
        }, "t1").start();
        // 確保t1執行緒先執行
        Thread.sleep(100);
        // 主執行緒中son.dosomething方法中需要呼叫f1的dosomething方法,f1的鎖被t1搶佔,必須等t1釋放鎖之後主執行緒才能進入
        s1.dosomething();
    }
}

預測結果:
the mysomething method of father
the dosomething method of son
三秒之後列印下面內容
the dosomething method of father

預測結果分析:
t1呼叫son.mydosomething方法時不需要獲取s1物件的鎖,但是son.mydosomething方法中呼叫了super.mydosomething()方法,獲取到f1例項的鎖,列印“the mysomething method of father”,然後停頓三秒鐘。
主執行緒停頓100毫秒後執行s1.dosomething方法,該方法需要獲取s1例項的鎖(獲取成功,因為t1沒有獲取s1例項的鎖),列印“the dosomething method of son”,接下來呼叫super.dosomething,需要獲取f1例項的鎖(獲取失敗,f1已經被t1獲取,三秒後才會釋放f1的鎖),主執行緒阻塞,三秒之後列印“the dosomething method of father

真實結果:
the mysomething method of father
三秒之後列印下面內容
the dosomething method of son
the dosomething method of father
結果分析:
看到真實結果後,覺得自己太傻太無知了。在真實結果面前,我好像感覺到了一絲絲真相的味道:好像t1在執行_Father類中的mydosomething方法時獲得是例項s1的鎖並不是f1的鎖,也就是說作者說的沒錯。

2. 第二步探索
這一次我乾脆一不做二不休,直接在_JavaConcurrency_01 中建立了一個Father類的例項f1來代替所有super。

class _Father{
    public synchronized void dosomething(){
        System.out.println("the dosomething method of father");
    }
    public synchronized void mydosomething() throws InterruptedException{
        System.out.println("the mysomething method of father");
        Thread.sleep(3000);
    }
}
public class _JavaConcurrency_01 extends _Father{
    _Father f1 = new _Father();
    public synchronized void dosomething() {
        System.out.println("the dosomething method of son");
        f1.dosomething();
    }
    public void mydosomething() throws InterruptedException {
         f1.mydosomething();
    }

    public static void main(String[] args) throws InterruptedException {
        final _JavaConcurrency_01 s1 = new _JavaConcurrency_01();
        new Thread(new Runnable() {
            public void run() {
                try {
                    s1.mydosomething();
                } catch (InterruptedException e) {
                }
            }
        }, "t1").start();
        Thread.sleep(100);
        s1.dosomething();
    }
}

預測結果:
the mysomething method of father
the dosomething method of son
三秒之後列印下面內容
the dosomething method of father

預測結果分析:
與第一步探索雷同

真實結果:
與預測結果完全一致,也就說我的說法好像不太對,真想打自己的臉。

3. 第三步探索
s1.mydosomething() ->super.mydosomething()
這次我要探索的是super.mydosomething()方法呼叫時,預設會傳遞一個“this”引數,而且this指向呼叫此方法的例項。我就是想看看這個預設的this指向了s1還是我說的f1。

class Father {
    public void doSomething() {
        System.out.print(this);
    }
    public String toString() {
        return "Father";
    }
}
public class Child extends Father {
    public void doSomething() {
        super.doSomething();
    }
    public String toString() {
        return "Son";
    }
    public static void main(String[] args) {
        Child child = new Child();
        child.doSomething();
    }
}

這裡我就不再預測了,人得學會有自知之明呀,直接上結果,結果很可怕,至少我這麼覺得,因為太無知,受不了一點驚嚇。

真實結果
Son
結果分析,原來super.mydosomething()中預設的”this”引數指向了s1,我的天呢,作者說的一點都沒有錯呀,真實重入呀。

4. 第四步探索
趕緊利用 javap -verbose Child 命令看了看位元組碼命令是怎麼執行的。

public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=2, Args_size=1
   0:   new     #1; //class _1/Child
   3:   dup
   4:   invokespecial   #23; //Method "<init>":()V
   7:   astore_1
   8:   aload_1
   9:   invokevirtual   #24; //Method doSomething:()V
   12:  return

位元組碼命令描述:
0-7行建立了Child型別的一個例項,也就是s1。
astore_1,將運算元棧頂引用型別數值存入本地變量表的第二個(從零開始計數)本地變數位置,也就是把s1存入本地變量表的第一個位置。
aload_1,將s1在推入棧頂。
invokevirtual,執行例項方法doSomething,此時傳入的“this”為棧頂元素s1。

public void doSomething();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #15; //Method _1/Father.doSomething:()V
   4:   return

現在我們來看關鍵的doSomething方法
0: aload_0,把this推入棧頂,這個“this”是s1的引用。
1: invokespecial #15; 呼叫父類方法,但是預設傳入的“this”引數仍舊指向s1

4 請原諒我的無知:

最後,我找到了一個說服自己的理由,但是其中還是有好多問題,知其然不知其所以然,希望在未來的日子裡可以慢慢解決這些問題,讓自己變得有學問起來,哈哈。