1. 程式人生 > >java synchronized的理解以及內建鎖和物件鎖

java synchronized的理解以及內建鎖和物件鎖

在java程式設計中,經常需要用到同步,而用得最多的也許是synchronized關鍵字了,下面看看這個關鍵字的用法。
因為synchronized關鍵字涉及到鎖的概念,所以先來了解一些相關的鎖知識。

java的內建鎖:每個java物件都可以用做一個實現同步的鎖,這些鎖成為內建鎖。執行緒進入同步程式碼塊或方法的時候會自動獲得該鎖,在退出同步程式碼塊或方法時會釋放該鎖。獲得內建鎖的唯一途徑就是進入這個鎖的保護的同步程式碼塊或方法。

java內建鎖是一個互斥鎖,這就是意味著最多隻有一個執行緒能夠獲得該鎖,當執行緒A嘗試去獲得執行緒B持有的內建鎖時,執行緒A必須等待或者阻塞,知道執行緒B釋放這個鎖,如果B執行緒不釋放這個鎖,那麼A執行緒將永遠等待下去。

java的物件鎖和類鎖:java的物件鎖和類鎖在鎖的概念上基本上和內建鎖是一致的,但是,兩個鎖實際是有很大的區別的,物件鎖是用於物件例項方法,或者一個物件例項上的,類鎖是用於類的靜態方法或者一個類的class物件上的。我們知道,類的物件例項可以有很多個,但是每個類只有一個class物件,所以不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定例項方法和靜態方法的區別的

上面已經對鎖的一些概念有了一點了解,下面探討synchronized關鍵字的用法。

synchronized的用法:synchronized修飾方法和synchronized修飾程式碼塊。

下面分別分析這兩種用法在物件鎖和類鎖上的效果。

物件鎖的synchronized修飾方法和程式碼塊:
Java程式碼 收藏程式碼
public class TestSynchronized
{
public void test1()
{
synchronized(this)
{
int i = 5;
while( i– > 0)
{
System.out.println(Thread.currentThread().getName() + ” : ” + i);
try
{
Thread.sleep(500);
}
catch (InterruptedException ie)
{
}
}
}
}

public synchronized void test2()   
{    
     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 TestSynchronized myt2 = new TestSynchronized();    
     Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
     Thread test2 = new Thread(  new Runnable() {  public void run() { myt2.test2();   }  }, "test2"  );    
     test1.start();;    
     test2.start();    

// TestRunnable tr=new TestRunnable();
// Thread test3=new Thread(tr);
// test3.start();
}

}

Java程式碼 收藏程式碼
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0

上述的程式碼,第一個方法時用了同步程式碼塊的方式進行同步,傳入的物件例項是this,表明是當前物件,當然,如果需要同步其他物件例項,也不可傳入其他物件的例項;第二個方法是修飾方法的方式進行同步。因為第一個同步程式碼塊傳入的this,所以兩個同步程式碼所需要獲得的物件鎖都是同一個物件鎖,下面main方法時分別開啟兩個執行緒,分別呼叫test1和test2方法,那麼兩個執行緒都需要獲得該物件鎖,另一個執行緒必須等待。上面也給出了執行的結果可以看到:直到test2執行緒執行完畢,釋放掉鎖,test1執行緒才開始執行。(可能這個結果有人會有疑問,程式碼裡面明明是先開啟test1執行緒,為什麼先執行的是test2呢?這是因為java編譯器在編譯成位元組碼的時候,會對程式碼進行一個重排序,也就是說,編譯器會根據實際情況對程式碼進行一個合理的排序,編譯前程式碼寫在前面,在編譯後的位元組碼不一定排在前面,所以這種執行結果是正常的, 這裡是題外話,最主要是檢驗synchronized的用法的正確性)

如果我們把test2方法的synchronized關鍵字去掉,執行結果會如何呢?
Java程式碼 收藏程式碼
test1 : 4
test2 : 4
test2 : 3
test1 : 3
test1 : 2
test2 : 2
test2 : 1
test1 : 1
test2 : 0
test1 : 0

上面是執行結果,我們可以看到,結果輸出是交替著進行輸出的,這是因為,某個執行緒得到了物件鎖,但是另一個執行緒還是可以訪問沒有進行同步的方法或者程式碼。進行了同步的方法(加鎖方法)和沒有進行同步的方法(普通方法)是互不影響的,一個執行緒進入了同步方法,得到了物件鎖,其他執行緒還是可以訪問那些沒有同步的方法(普通方法)。這裡涉及到內建鎖的一個概念(此概念出自java併發程式設計實戰第二章):物件的內建鎖和物件的狀態之間是沒有內在的關聯的,雖然大多數類都將內建鎖用做一種有效的加鎖機制,但物件的域並不一定通過內建鎖來保護。當獲取到與物件關聯的內建鎖時,並不能阻止其他執行緒訪問該物件,當某個執行緒獲得物件的鎖之後,只能阻止其他執行緒獲得同一個鎖。之所以每個物件都有一個內建鎖,是為了免去顯式地建立鎖物件。

所以synchronized只是一個內建鎖的加鎖機制,當某個方法加上synchronized關鍵字後,就表明要獲得該內建鎖才能執行,並不能阻止其他執行緒訪問不需要獲得該內建鎖的方法。

類鎖的修飾(靜態)方法和程式碼塊:
Java程式碼 收藏程式碼
public class TestSynchronized
{
public void test1()
{
synchronized(TestSynchronized.class)
{
int i = 5;
while( i– > 0)
{
System.out.println(Thread.currentThread().getName() + ” : ” + i);
try
{
Thread.sleep(500);
}
catch (InterruptedException ie)
{
}
}
}
}

public static synchronized void test2()   
{    
     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 TestSynchronized myt2 = new TestSynchronized();    
     Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
     Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );    
     test1.start();    
     test2.start();    

// TestRunnable tr=new TestRunnable();
// Thread test3=new Thread(tr);
// test3.start();
}

}

Java程式碼 收藏程式碼
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

其實,類鎖修飾方法和程式碼塊的效果和物件鎖是一樣的,因為類鎖只是一個抽象出來的概念,只是為了區別靜態方法的特點,因為靜態方法是所有物件例項共用的,所以對應著synchronized修飾的靜態方法的鎖也是唯一的,所以抽象出來個類鎖。其實這裡的重點在下面這塊程式碼,synchronized同時修飾靜態和非靜態方法
Java程式碼 收藏程式碼
public class TestSynchronized
{
public synchronized void test1()
{
int i = 5;
while( i– > 0)
{
System.out.println(Thread.currentThread().getName() + ” : ” + i);
try
{
Thread.sleep(500);
}
catch (InterruptedException ie)
{
}
}
}

public static synchronized void test2()   
{    
     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 TestSynchronized myt2 = new TestSynchronized();    
     Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
     Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );    
     test1.start();    
     test2.start();    

// TestRunnable tr=new TestRunnable();
// Thread test3=new Thread(tr);
// test3.start();
}

}

Java程式碼 收藏程式碼
test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面程式碼synchronized同時修飾靜態方法和例項方法,但是執行結果是交替進行的,這證明了類鎖和物件鎖是兩個不一樣的鎖,控制著不同的區域,它們是互不干擾的。同樣,執行緒獲得物件鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。

到這裡,對synchronized的用法已經有了一定的瞭解。這時有一個疑問,既然有了synchronized修飾方法的同步方式,為什麼還需要synchronized修飾同步程式碼塊的方式呢?而這個問題也是synchronized的缺陷所在

synchronized的缺陷:當某個執行緒進入同步方法獲得物件鎖,那麼其他執行緒訪問這裡物件的同步方法時,必須等待或者阻塞,這對高併發的系統是致命的,這很容易導致系統的崩潰。如果某個執行緒在同步方法裡面發生了死迴圈,那麼它就永遠不會釋放這個物件鎖,那麼其他執行緒就要永遠的等待。這是一個致命的問題。

當然同步方法和同步程式碼塊都會有這樣的缺陷,只要用了synchronized關鍵字就會有這樣的風險和缺陷。既然避免不了這種缺陷,那麼就應該將風險降到最低。這也是同步程式碼塊在某種情況下要優於同步方法的方面。例如在某個類的方法裡面:這個類裡面聲明瞭一個物件例項,SynObject so=new SynObject();在某個方法裡面呼叫了這個例項的方法so.testsy();但是呼叫這個方法需要進行同步,不能同時有多個執行緒同時執行呼叫這個方法。
這時如果直接用synchronized修飾呼叫了so.testsy();程式碼的方法,那麼當某個執行緒進入了這個方法之後,這個物件其他同步方法都不能給其他執行緒訪問了。假如這個方法需要執行的時間很長,那麼其他執行緒會一直阻塞,影響到系統的效能。
如果這時用synchronized來修飾程式碼塊:synchronized(so){so.testsy();},那麼這個方法加鎖的物件是so這個物件,跟執行這行程式碼的物件沒有關係,當一個執行緒執行這個方法時,這對其他同步方法時沒有影響的,因為他們持有的鎖都完全不一樣。

不過這裡還有一種特例,就是上面演示的第一個例子,物件鎖synchronized同時修飾方法和程式碼塊,這時也可以體現到同步程式碼塊的優越性,如果test1方法同步程式碼塊後面有非常多沒有同步的程式碼,而且有一個100000的迴圈,這導致test1方法會執行時間非常長,那麼如果直接用synchronized修飾方法,那麼在方法沒執行完之前,其他執行緒是不可以訪問test2方法的,但是如果用了同步程式碼塊,那麼當退出程式碼塊時就已經釋放了物件鎖,當執行緒還在執行test1的那個100000的迴圈時,其他執行緒就已經可以訪問test2方法了。這就讓阻塞的機會或者執行緒更少。讓系統的效能更優越。

一個類的物件鎖和另一個類的物件鎖是沒有關聯的,當一個執行緒獲得A類的物件鎖時,它同時也可以獲得B類的物件鎖。

打個比方:一個object就像一個大房子,大門永遠開啟。房子裡有 很多房間(也就是方法)。

這些房間有上鎖的(synchronized方法), 和不上鎖之分(普通方法)。房門口放著一把鑰匙(key),這把鑰匙可以開啟所有上鎖的房間。

另外我把所有想呼叫該物件方法的執行緒比喻成想進入這房子某個 房間的人。所有的東西就這麼多了,下面我們看看這些東西之間如何作用的。

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

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

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

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

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

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

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

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

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

     為什麼要使用同步程式碼塊呢?我想應該是這樣的:首先對程式來講同步的部分很影響執行效率,而一個方法通常是先建立一些區域性變數,再對這些變數做一些 操作,如運算,顯示等等;而同步所覆蓋的程式碼越多,對效率的影響就越嚴重。因此我們通常儘量縮小其影響範圍。

如何做?同步程式碼塊。我們只把一個方法中該同 步的地方同步,比如運算。

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

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

大概就這麼多了。