對synchronized(this)的一些理解

對synchronized(this)的一些理解

轉載。只要把這幾個例子搞明白了,基本上synchronized的用法可以說沒問題了。做多線程開發不能不看!

一、當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。

二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

四、第三個例子同樣適用其它同步代碼塊或同步方法。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分或同步方法的訪問都被暫時阻塞。

五、以上規則對其它對象鎖同樣適用.

舉例說明:
一、當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。

package ths;

public class Thread1 implements Runnable {
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + /" synchronized loop /" + i);
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread ta = new Thread(t1, /"A/");
Thread tb = new Thread(t1, /"B/");
ta.start();
tb.start();
}
}

結果:
A synchronized loop 0
A synchronized loop 1
A synchronized loop 2
A synchronized loop 3
A synchronized loop 4
B synchronized loop 0
B synchronized loop 1
B synchronized loop 2
B synchronized loop 3
B synchronized loop 4

二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

package ths;

public class Thread2 {
public void m4t1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + /" : /" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
public void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + /" : /" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
public static void main(String[] args) {
final Thread2 myt2 = new Thread2();
Thread t1 = new Thread(
new Runnable() {
public void run() {
myt2.m4t1();
}
}, /"t1/"
);
Thread t2 = new Thread(
new Runnable() {
public void run() {
myt2.m4t2();
}
}, /"t2/"
);
t1.start();
t2.start();
}
}

結果:
t1 : 4
t2 : 4
t1 : 3
t2 : 3
t1 : 2
t2 : 2
t1 : 1
t2 : 1
t1 : 0
t2 : 0

三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

//修改Thread2.m4t2()方法:
public void m4t2() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + /" : /" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}

}

結果:

t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0

四、第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。

//修改Thread2.m4t2()方法如下:

public synchronized void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + /" : /" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}

結果:
t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0

五、以上規則對其它對象鎖同樣適用:

package ths;

public class Thread3 {
class Inner {
    private void m4t1() {
        int i = 5;
        while(i-- > 0) {
              System.out.println(Thread.currentThread().getName() + /" : Inner.m4t1()=/" + i);
              try {
                  Thread.sleep(500);
              } catch(InterruptedException ie) {
              }
        }
    }
    private void m4t2() {
        int i = 5;
        while(i-- > 0) {
              System.out.println(Thread.currentThread().getName() + /" : Inner.m4t2()=/" + i);
              try {
                  Thread.sleep(500);
              } catch(InterruptedException ie) {
              }
        }
    }
}
    private void m4t1(Inner inner) {
        synchronized(inner) { //使用對象鎖
              inner.m4t1();
        }
    }
    private void m4t2(Inner inner) {
        inner.m4t2();
    }
    public static void main(String[] args) {
        final Thread3 myt3 = new Thread3();
        final Inner inner = myt3.new Inner();
        Thread t1 = new Thread(
              new Runnable() {
              public void run() {
                  myt3.m4t1(inner);
              }
              }, /"t1/"
        );
        Thread t2 = new Thread(
              new Runnable() {
              public void run() {
                  myt3.m4t2(inner);
              }
              }, /"t2/"
        );
        t1.start();
        t2.start();
    }
}

結果:

儘管線程t1獲得了對Inner的對象鎖,但由於線程t2訪問的是同一個Inner中的非同步部分。所以兩個線程互不干擾。

t1 : Inner.m4t1()=4
t2 : Inner.m4t2()=4
t1 : Inner.m4t1()=3
t2 : Inner.m4t2()=3
t1 : Inner.m4t1()=2
t2 : Inner.m4t2()=2
t1 : Inner.m4t1()=1
t2 : Inner.m4t2()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=0

現在在Inner.m4t2()前面加上synchronized:

private synchronized void m4t2() {
    int i = 5;
    while(i-- > 0) {
        System.out.println(Thread.currentThread().getName() + /" : Inner.m4t2()=/" + i);
        try {
              Thread.sleep(500);
        } catch(InterruptedException ie) {
        }
    }
}

結果:

儘管線程t1與t2訪問了同一個Inner對象中兩個毫不相關的部分,但因爲t1先獲得了對Inner的對象鎖,所以t2對Inner.m4t2()的訪問也被阻塞,因爲m4t2()是Inner中的一個同步方法。

t1 : Inner.m4t1()=4
t1 : Inner.m4t1()=3
t1 : Inner.m4t1()=2
t1 : Inner.m4t1()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=4
t2 : Inner.m4t2()=3
t2 : Inner.m4t2()=2
t2 : Inner.m4t2()=1
t2 : Inner.m4t2()=0

From: http://www.54bk.com/more.asp?name=kevindeng&id=2239&commentid=14871
還有一個形象解釋,也不錯
圖片:
圖例說明:左邊帶麻點的是synchronized方法或代碼塊。右邊的是普通方法。黃色的是該對象(也稱之爲實例)的key,每個對象有且只有一個key。只有擁有該key的線程才能進入synchronized方法。

打個比方:一個object就像一個大房子,大門永遠打開。房子裏有很多房間(也就是方法)。這些房間有上鎖的(synchronized方法),和不上鎖之分(普通方法)。房門口放着一把鑰匙(key),這把鑰匙可以打開所有上鎖的房間。另外我把所有想調用該對象方法的線程比喻成想進入這房子某個房間的人。所有的東西就這麼多了,下面我們看看這些東西之間如何作用的。

在此我們先來明確一下我們的前提條件。該對象至少有一個synchronized方法,否則這個key還有啥意義。當然也就不會有我們的這個主題了。

一個人想進入某間上了鎖的房間,他來到房子門口,看見鑰匙在那兒(說明暫時還沒有其他人要使用上鎖的房間)。於是他走上去拿到了鑰匙,並且按照自己的計劃使用那些房間。注意一點,他每次使用完一次上鎖的房間後會馬上把鑰匙還回去。即使他要連續使用兩間上鎖的房間,中間他也要把鑰匙還回去,再取回來。

因此,普通情況下鑰匙的使用原則是:“隨用隨借,用完即還。”

這時其他人可以不受限制的使用那些不上鎖的房間,一個人用一間可以,兩個人用一間也可以,沒限制。但是如果當某個人想要進入上鎖的房間,他就要跑到大門口去看看了。有鑰匙當然拿了就走,沒有的話,就只能等了。

要是很多人在等這把鑰匙,等鑰匙還回來以後,誰會優先得到鑰匙?Not guaranteed。象前面例子裏那個想連續使用兩個上鎖房間的傢伙,他中間還鑰匙的時候如果還有其他人在等鑰匙,那麼沒有任何保證這傢伙能再次拿到。(JAVA規範在很多地方都明確說明不保證,象Thread.sleep()休息後多久會返回運行,相同優先權的線程那個首先被執行,當要訪問對象的鎖被釋放後處於等待池的多個線程哪個會優先得到,等等。我想最終的決定權是在JVM,之所以不保證,就是因爲JVM在做出上述決定的時候,絕不是簡簡單單根據一個條件來做出判斷,而是根據很多條。而由於判斷條件太多,如果說出來可能會影響JAVA的推廣,也可能是因爲知識產權保護的原因吧。SUN給了個不保證就混過去了。無可厚非。但我相信這些不確定,並非完全不確定。因爲計算機這東西本身就是按指令運行的。即使看起來很隨機的現象,其實都是有規律可尋。學過計算機的都知道,計算機裏隨機數的學名是僞隨機數,是人運用一定的方法寫出來的,看上去隨機罷了。另外,或許是因爲要想弄的確定太費事,也沒多大意義,所以不確定就不確定了吧。)

再來看看同步代碼塊。和同步方法有小小的不同。

1.從尺寸上講,同步代碼塊比同步方法小。你可以把同步代碼塊看成是沒上鎖房間裏的一塊用帶鎖的屏風隔開的空間。

2.同步代碼塊還可以人爲的指定獲得某個其它對象的key。就像是指定用哪一把鑰匙才能開這個屏風的鎖,你可以用本房的鑰匙;你也可以指定用另一個房子的鑰匙才能開,這樣的話,你要跑到另一棟房子那兒把那個鑰匙拿來,並用那個房子的鑰匙來打開這個房子的帶鎖的屏風。

記住你獲得的那另一棟房子的鑰匙,並不影響其他人進入那棟房子沒有鎖的房間。

爲什麼要使用同步代碼塊呢?我想應該是這樣的:首先對程序來講同步的部分很影響運行效率,而一個方法通常是先創建一些局部變量,再對這些變量做一些操作,如運算,顯示等等;而同步所覆蓋的代碼越多,對效率的影響就越嚴重。因此我們通常儘量縮小其影響範圍。如何做?同步代碼塊。我們只把一個方法中該同步的地方同步,比如運算。

另外,同步代碼塊可以指定鑰匙這一特點有個額外的好處,是可以在一定時期內霸佔某個對象的key。還記得前面說過普通情況下鑰匙的使用原則嗎。現在不是普通情況了。你所取得的那把鑰匙不是永遠不還,而是在退出同步代碼塊時才還。

還用前面那個想連續用兩個上鎖房間的傢伙打比方。怎樣才能在用完一間以後,繼續使用另一間呢。用同步代碼塊吧。先創建另外一個線程,做一個同步代碼塊,把那個代碼塊的鎖指向這個房子的鑰匙。然後啓動那個線程。只要你能在進入那個代碼塊時抓到這房子的鑰匙,你就可以一直保留到退出那個代碼塊。也就是說你甚至可以對本房內所有上鎖的房間遍歷,甚至再sleep(10*60*1000),而房門口卻還有1000個線程在等這把鑰匙呢。很過癮吧。

在此對sleep()方法和鑰匙的關聯性講一下。一個線程在拿到key後,且沒有完成同步的內容時,如果被強制sleep()了,那key還一直在它那兒。直到它再次運行,做完所有同步內容,纔會歸還key。記住,那傢伙只是幹活幹累了,去休息一下,他並沒幹完他要乾的事。爲了避免別人進入那個房間把裏面搞的一團糟,即使在睡覺的時候他也要把那唯一的鑰匙戴在身上。

最後,也許有人會問,爲什麼要一把鑰匙通開,而不是一個鑰匙一個門呢?我想這純粹是因爲複雜性問題。一個鑰匙一個門當然更安全,但是會牽扯好多問題。鑰匙的產生,保管,獲得,歸還等等。其複雜性有可能隨同步方法的增加呈幾何級數增加,嚴重影響效率。

這也算是一個權衡的問題吧。爲了增加一點點安全性,導致效率大大降低,是多麼不可取啊。

From: http://www.54bk.com/more.asp?name=czp&id=2097
一個synchronized使用不當導致多進程互鎖的例子
這個程序之所以能正常運行,是因爲每個thread都非常之快地運行結束。

public class SyncTest{
public static void main(String[] args){
final StringBuffer s1=new StringBuffer();
final StringBuffer s2=new StringBuffer();
new Thread() {
public void run(){ //只有擁有s1的鎖,纔可以執行後面的代碼,
  synchronized(s1){   //現在當前線程有S1的鎖
    s1.append("A");  
    synchronized(s2){   // 當前線程有S2的鎖
    s2.append("B");
    System.out.print(s1);
    System.out.print(s2);
    }  
  }
  }
}.start(); // 如果足夠快的話,當前線程結束運行,釋放S1和S2的鎖。

new Thread(){ //此時上一個線程可能已經結束,S1和S2的鎖都已經釋放。
public void run(){
  synchronized(s2){ //當前線程有S2的鎖
    s2.append("C");
synchronized(s1){ //當前線程有S2的鎖
  s1.append("D");
  System.out.println(s2);
  System.out.println(s1);
  }
}
}
}.start();
}
}


  你可以試驗一下,在兩個線程中各加上幾個yield(),當第一個線程剛剛得到S1時,第二個線程已經得到了S2的鎖。然後第一個線程在等S2,第二個線程等S1,就會形成死鎖。
  Java本身並沒有提供避免這種死鎖的方法,只有靠程序員自己去注意了。因此,良好的程序設計方法是,(儘量)保持同樣的順序去獲取鎖。
wait/notify機制
通常,多線程之間需要協調工作。例如,瀏覽器的一個顯示圖片的線程displayThread想要執行顯示圖片的任務,必須等待下載線程downloadThread將該圖片下載完畢。如果圖片還沒有下載完,displayThread可以暫停,當downloadThread完成了任務後,再通知displayThread“圖片準備完畢,可以顯示了”,這時,displayThread繼續執行。



以上邏輯簡單的說就是:如果條件不滿足,則等待。當條件滿足時,等待該條件的線程將被喚醒。在Java中,這個機制的實現依賴於wait/notify。等待機制與鎖機制是密切關聯的。例如:



synchronized(obj) {
  while(!condition) {
    obj.wait();
  }
  obj.doSomething();
}



當線程A獲得了obj鎖後,發現條件condition不滿足,無法繼續下一處理,於是線程A就wait()。



在另一線程B中,如果B更改了某些條件,使得線程A的condition條件滿足了,就可以喚醒線程A:



synchronized(obj) {
  condition = true;
  obj.notify();
}



需要注意的概念是:



# 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) {...} 代碼段內。



# 調用obj.wait()後,線程A就釋放了obj的鎖,否則線程B無法獲得obj鎖,也就無法在synchronized(obj) {...} 代碼段內喚醒A。



# 當obj.wait()方法返回後,線程A需要再次獲得obj鎖,才能繼續執行。



# 如果A1,A2,A3都在obj.wait(),則B調用obj.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)。



# obj.notifyAll()則能全部喚醒A1,A2,A3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,因此,A1,A2,A3只有一個有機會獲得鎖繼續執行,例如A1,其餘的需要等待A1釋放obj鎖之後才能繼續執行。



# 當B調用obj.notify/notifyAll的時候,B正持有obj鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得obj鎖。直到B退出synchronized塊,釋放obj鎖後,A1,A2,A3中的一個纔有機會獲得鎖繼續執行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章