看到網上很多講synchronized關鍵字用法的文章,說的都很有道理,也很深刻,但是看完總感覺腦袋里還是有點亂亂的。經過一番自己的思考后,想從自己的思考角度出發,來說一說synchronized關鍵字的用法。在我看來,其實想將加鎖后的訪問規則講清楚其實很簡單。廢話就不多說了,看下面的文字吧。
一、一套清晰的規則
其實很容易發現,無論是加了鎖的方法還是加了鎖的代碼塊,無是加的是對象鎖還是加的是類鎖,只要上的是同一把鎖,那么它們的訪問規則就應該是相同的。也就是上了同一把鎖的東西,要么一起可以被訪問,要么一起禁止別人訪問。因此,如果想搞清楚訪問的規則,我們首先要搞清楚鎖的類型。然后判斷,只要上的是同一把鎖,訪問的規則就應該相同。那么java中的鎖有那些類型呢。可以簡單的總結為兩種類型:
java中共有兩種類型的鎖:
(1)類鎖:只有synchronized修飾靜態方法或者修飾一個類的class對象時,才是類鎖。
(2)對象鎖:處了類鎖,所有其他的上鎖方式都認為是對象鎖。比如synchronized修飾普通方法或者synchronized(this)給代碼塊上鎖等
應該注意的是,因為一個類只有一個.class對象,因此所有的訪問者在訪問被加了類鎖的東西時,都是共用同一把鎖,而類的實例卻可以有很多個,因此不同對象訪問加了對象鎖的東西,它們的訪問互不干擾。
知道了鎖的類型,那么我們就可以總結出一個通用且清晰的規則了。如下:
這就是終極的規則:
其實這個規則很簡單:
(1)加了相同鎖的東西,它們的訪問規則是相同的,即當某個訪問者獲得該鎖時,它們一起向該訪問者開放訪問,向其他沒有獲得該鎖的訪問者關閉訪問;
(2)加了不同鎖的東西訪問互相不干擾
(3)而沒有加鎖的東西隨時都可以任意訪問,不受任何限制。
然后再來看怎么判斷什么情況下是相同的鎖。如下:
怎么判斷是一把相同的鎖,遵循下面的規則:
(1)不同類型的鎖不是同一把鎖。
(2)加的是對象鎖,那么必須是同一個對象實例才是同一把鎖
(3)加的是類鎖,那必須是同一類才是同一把鎖。
好了,也就是說我們判斷訪問的規則,就是基於個步驟:
1.首先判斷是不是同一把鎖
2.然后判斷各自的訪問規則
下面我們來看幾個實際例子,來看看我們這個規則到底怎么使用。
二、使用這個規則
(1)實驗一
新建一個java項目,新建一個SynTest類,在其中寫如下代碼:
1 public class SynTest { 2 3 private synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我們是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(this){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我們是test2"); 13 } 14 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 st.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
在代碼中,可以看到我們給test1方法上了對象鎖,加鎖的方式是給方法加鎖,加的是當前對象鎖;而給test2方法上也上了對象鎖,加鎖的方式給代碼塊加鎖,注意上的是當前對象鎖,你也可以將this替換成任意其他對象。然后我們在main方法中看到,新建了兩個線程,都是用同一個對象來調用這兩個方法。因此,可以判定線程t1和t2使用的是同一把鎖,因此訪問規則就是t1獲得了這把鎖后,tes1方法和test2中被加鎖的代碼塊都允許t1訪問,都拒絕t2訪問,等t1運行完,t2才會獲得該鎖,進行訪問。因此輸出的結果很明顯了,t1執行完,再執行t2。我們運行下程序,看看我們按照規則來推理的是否正確,運行結果如下:

和我們預想的完全一樣!!
然后我們再做個試驗,比如將main方法中的代碼改成下面的:
1 public static void main(String[] args) { 2 final SynTest st = new SynTest(); 3 final SynTest st2 = new SynTest(); 4 Thread t1 = new Thread(new Runnable(){public void run() { 5 st.test1(); 6 }}); 7 Thread t2 = new Thread(new Runnable(){public void run() { 8 st2.test2(); 9 }}); 10 t1.start(); 11 t2.start(); 12 }
只是多出一個實例而已,然后線程t2通過st2來調用test2。那么就test1和test2加的都是當前對象的鎖,顯然它們的當前對象不同吧。所以它們不是同一把鎖,互相不干擾。那么我們運行程序,效果如下:

果然它們各自運行各自的,所以沒什么順序。
怎么樣,這樣子來判斷訪問權限是不是很簡單啊。
(2)實驗二
然后修改SynTest的代碼,如下:
1 public class SynTest { 2 3 private static synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我們是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(SynTest.class){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我們是test2"); 13 } 14 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 SynTest.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
很簡單,test1是一個靜態方法,所以它加的鎖是類鎖,而test2加了一個類鎖,是加載了一個代碼塊中,因此他們加的是相同的鎖。運行程序,結果如下:

即t1訪問完,t2再訪問。
然后我們再修改SynTest中的代碼,如下:
1 public class SynTest { 2 3 private static synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我們是test1"); 6 } 7 } 8 9 private synchronized void test2(){ 10 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我們是test2"); 13 14 } 15 } 16 17 public static void main(String[] args) { 18 final SynTest st = new SynTest(); 19 Thread t1 = new Thread(new Runnable(){public void run() { 20 SynTest.test1(); 21 }}); 22 Thread t2 = new Thread(new Runnable(){public void run() { 23 st.test2(); 24 }}); 25 t1.start(); 26 t2.start(); 27 } 28 29 }
代碼中很顯然了,test1是靜態方法,它上的是類鎖。而test2是普通方法,它上的對象鎖。這是不同的鎖。所以t1訪問test1時,test2方法是不受干擾的,t2肯定可以同時訪問test2.因此打印順序為任意順序。如下:

為亂序。此時如果你的打印結果為先打印出t1的結果再是t2的結果,也不必驚訝,因為程序簡單,循環次數少,CPU性能高,所以很可能t2缸啟動就瞬間運行完了t1。
(3)實驗三
我們再修改SynTest的代碼,如下:
1 public class SynTest { 2 3 private synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我們是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(SynTest.class){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我們是test2"); 13 14 } 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 st.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
很明顯一個類鎖,一個對象鎖,訪問規則互不干擾。即,t1訪問test1方法時,並不影響t2訪問test2(但是會禁止t2訪問test1,因為t2用的也是st對象,此時t1已給st對象上鎖了)。所以打印順序可能為任意順序。好了,運行程序,結果如下:

注意:此時如果你的打印結果為先打印出t1的結果再是t2的結果,也不必驚訝,因為程序簡單,循環次數少,CPU性能高,所以很可能t2缸啟動就瞬間運行完了t1。
好了,還有很多的實驗我們就不再做了,都是根據上面總結的規則來做的。相信這下,你對上鎖和上鎖后的訪問規則都清楚了吧。
三、如果你還不理解的話
如果你還不理解的話,這是某位大牛做的一個很好的比喻。你可以看看。摘抄如下:
打個比方:一個object就像一個大房子,大門永遠打開。房子里有 很多房間(也就是方法)。 這些房間有上鎖的(synchronized方法), 和不上鎖之分(普通方法)。房門口放着一把鑰匙(key),這把鑰匙可以打開所有上鎖的房間。 另外我把所有想調用該對象方法的線程比喻成想進入這房子某個 房間的人。所有的東西就這么多了,下面我們看看這些東西之間如何作用的。 在此我們先來明確一下我們的前提條件。該對象至少有一個synchronized方法,否則這個key還有啥意義。當然也就不會有我們的這個主題了。 一個人想進入某間上了鎖的房間,他來到房子門口,看見鑰匙在那兒(說明暫時還沒有其他人要使用上鎖的 房間)。於是他走上去拿到了鑰匙,並且按照自己 的計划使用那些房間。注意一點,他每次使用完一次上鎖的房間后會馬上把鑰匙還回去。即使他要連續使用兩間上鎖的房間,中間他也要把鑰匙還回去,再取回來。 因此,普通情況下鑰匙的使用原則是:“隨用隨借,用完即還。” 這時其他人可以不受限制的使用那些不上鎖的房間,一個人用一間可以,兩個人用一間也可以,沒限制。但是如果當某個人想要進入上鎖的房間,他就要跑到大門口去看看了。有鑰匙當然拿了就走,沒有的話,就只能等了。 要是很多人在等這把鑰匙,等鑰匙還回來以后,誰會優先得到鑰匙?Not guaranteed。象前面例子里那個想連續使用兩個上鎖房間的家伙,他中間還鑰匙的時候如果還有其他人在等鑰匙,那么沒有任何保證這家伙能再次拿到。 (JAVA規范在很多地方都明確說明不保證,像Thread.sleep()休息后多久會返回運行,相同優先權的線程那個首先被執行,當要訪問對象的鎖被 釋放后處於等待池的多個線程哪個會優先得到,等等。我想最終的決定權是在JVM,之所以不保證,就是因為JVM在做出上述決定的時候,絕不是簡簡單單根據 一個條件來做出判斷,而是根據很多條。而由於判斷條件太多,如果說出來可能會影響JAVA的推廣,也可能是因為知識產權保護的原因吧。SUN給了個不保證 就混過去了。無可厚非。但我相信這些不確定,並非完全不確定。因為計算機這東西本身就是按指令運行的。即使看起來很隨機的現象,其實都是有規律可尋。學過 計算機的都知道,計算機里隨機數的學名是偽隨機數,是人運用一定的方法寫出來的,看上去隨機罷了。另外,或許是因為要想弄的確太費事,也沒多大意義,所 以不確定就不確定了吧。) 再來看看同步代碼塊。和同步方法有小小的不同。 1.從尺寸上講,同步代碼塊比同步方法小。你可以把同步代碼塊看成是沒上鎖房間里的一塊用帶鎖的屏風隔開的空間。 2.同步代碼塊還可以人為的指定獲得某個其它對象的key。就像是指定用哪一把鑰匙才能開這個屏風的鎖,你可以用本房的鑰匙;你也可以指定用另一個房子的鑰匙才能開,這樣的話,你要跑到另一棟房子那兒把那個鑰匙拿來,並用那個房子的鑰匙來打開這個房子的帶鎖的屏風。 記住你獲得的那另一棟房子的鑰匙,並不影響其他人進入那棟房子沒有鎖的房間。 為什么要使用同步代碼塊呢?我想應該是這樣的:首先對程序來講同步的部分很影響運行效率,而一個方法通常是先創建一些局部變量,再對這些變量做一些 操作,如運算,顯示等等;而同步所覆蓋的代碼越多,對效率的影響就越嚴重。因此我們通常盡量縮小其影響范圍。 如何做?同步代碼塊。我們只把一個方法中該同 步的地方同步,比如運算。 另外,同步代碼塊可以指定鑰匙這一特點有個額外的好處,是可以在一定時期內霸占某個對象的key。還記得前面說過普通情況下鑰匙的使用原則嗎。現在不是普通情況了。你所取得的那把鑰匙不是永遠不還,而是在退出同步代碼塊時才還。 還用前面那個想連續用兩個上鎖房間的家伙打比方。怎樣才能在用完一間以后,繼續使用另一間呢。用同步代碼塊吧。先創建另外一個線程,做一個同步代碼 塊,把那個代碼塊的鎖指向這個房子的鑰匙。然后啟動那個線程。只要你能在進入那個代碼塊時抓到這房子的鑰匙,你就可以一直保留到退出那個代碼塊。也就是說 你甚至可以對本房內所有上鎖的房間遍歷,甚至再sleep(10*60*1000),而房門口卻還有1000個線程在等這把鑰匙呢。很過癮吧。
