三、6個常見的使用情況
我們先給出這6種常見的情況,然後一個一個分析。
1、兩個線程同時訪問一個對象的同步方法。
2、兩個線程訪問的是兩個對象的同步方法。
3、兩個線程訪問的是synchronized的靜態方法。
4、兩個線程同時訪問同步方法與非同步方法。
5、一個線程訪問一個類的兩個普通同步方法。
6、同時訪問靜態同步方法和非靜態同步方法。
為了對這6種情況做到心中有數,不至於搞混了,我們畫一張圖,對每一種情況進行分析。
上面是框架圖,下面我們基於開始來分析:
1、兩個線程同時訪問一個對象的同步方法
這種情況對應於以下這張圖:
這種情況很簡單,我們在上面也演示過,結果就是同一個時刻只能有一個方法進入。這裡就不再演示了。
2、兩個線程訪問的是兩個對象的同步方法
這種情況對應於下面這種:
也就是一個方法有兩把鎖,線程1和線程2互不干擾的訪問。鎖是不起作用的。
3、兩個線程訪問的是synchronized的靜態方法
這種情況對應於下面這種情況:
我們對這種情況來測試一下吧。
public class SynTest6 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest6 instance1 = new SynTest6(); SynTest6 instance2 = new SynTest6(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); } @Override public void run() { method1(); } public synchronized static void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了靜態方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開靜態方法,並釋放鎖"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在這個例子中我們實例化了兩個對象instance1和instance2,並且存放在了兩個不同的線程中,我們測試一下訪問同一個static同步方法你會發現。即使是實例不同,鎖也會生效,也就是同一時刻只能有一個線程進去。
4、兩個線程同時訪問同步方法與非同步方法
這種情況對應於下面這張圖:
我們對這種情況使用代碼進行演示一遍:
public class SynTest7 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest7 instance1 = new SynTest7(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance1); thread1.start(); thread2.start(); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了同步方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開同步方法"); } catch (InterruptedException e) { e.printStackTrace(); } } public void method2() { System.out.println(Thread.currentThread().getName() + "進入了普通方法"); System.out.println(Thread.currentThread().getName() + "離開了普通方法"); } }
在上面的代碼中,我們定義一個對象,但是使用了兩個線程去分別同時訪問同步和非同步方法。我們看結果:
也就是說,同步方法依然會同步執行,非同步方法不會受到任何影響。
5、一個線程訪問一個類的兩個普通同步方法
這種情況對應於下面這張圖:
我們代碼來測試一下:
public class SynTest8 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest8 instance1 = new SynTest8(); Thread thread1 = new Thread(instance1); thread1.start(); } @Override public void run() { if(Thread.currentThread().getName().equals("Thread-0")) { method1(); }else { method2(); } } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了同步方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開同步方法1"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method2() { try { System.out.println(Thread.currentThread().getName() + "進入到了同步方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開同步方法2"); } catch (InterruptedException e) { e.printStackTrace(); } } }
上面這個例子我們創建了一個對象instance1,然後使用一個線程分別去訪問同步方法1和同步方法2。結果呢可想而知,所一定會失效。因為在一開始我們已經驗證了,此時同步方法1和同步方法2中synchronized鎖的就是this對象,所以是同一把鎖。當然會生效。
6、同時訪問靜態同步方法和非靜態同步方法
這種情況對應於下面這張圖:
我們使用代碼來測試一波:
public class SynTest9 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest9 instance1 = new SynTest9(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance1); thread1.start();thread2.start(); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了同步方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開同步方法1"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized static void method2() { try { System.out.println(Thread.currentThread().getName() + "進入到了靜態同步方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開靜態同步方法2"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在上面的代碼中,我們創建了一個instance實例,使用兩個線程同時訪問普通同步方法和靜態同步方法。下面運行一下,看看輸出結果:
上面輸出結果表明普通同步方法和靜態同步方法是沒有關聯的,這是為什麼呢?這是因為普通同步方法的鎖是對象,但是靜態同步方法的鎖是類,所以這是兩把鎖。鎖自然也就是失效了。
四、性質
讀到這裡,不知道你是不是已經很疲憊了,反正我寫的是很難受,不過剩下的這些部分才是精華,也是面試或者是工作中提升你zhuangbility的一個點。希望你一定要注意。認真讀下去。
對於synchronized關鍵字主要有兩個性質:可重入性質和不可中斷性質。我們分別來看。
1、可重入性質
什麼是可重入呢?指的是同一線程的外層函數獲得鎖之後,內層函數可以直接再次獲取該鎖。我們舉一個例子來說明,一句話吃著碗裡的看著鍋裡的。嘴裡面還沒吃完就繼續再去拿吃的。這就是可重入。不可重入的意思正好相反,你吃完了這碗飯才能盛下一碗。
可重入的程度可以細分為三種情況,我們分別測試一下:
(1)同一個方法中是不是可重入的。就好比是遞歸調用同步方法。
(2)不同的方法是不是可重入的。就好比是一個同步方法調用另外一個同步方法。
(3)不同的類方法是不是可重入的。
下面我們就是用代碼來測試一遍:
(1)同一個方法是不是可重入的
public class SynTest10 { private int a=1; public static void main(String[] args) throws InterruptedException { SynTest10 instance1 = new SynTest10(); instance1.method1(); } public synchronized void method1() { System.out.println("method1: a= " + a); if(a == 3) { return ; }else { a++; method1(); } } }
代碼很簡單,也就是我們定義了一個變量a,只要a不等於3,就一直遞歸調用方法method1。我們可以看一下運行結果。
也就是說在同一個方法中是可重入的。下面我們接著測試。
(2)不同的方法是不是可重入的
public class SynTest10 { public static void main(String[] args) throws InterruptedException { SynTest10 instance1 = new SynTest10(); instance1.method1(); } public synchronized void method1() { System.out.println("method1"); method2(); } public synchronized void method2() { System.out.println("method2" ); } }
我們在同步方法1中調用了同步方法2。我們同樣測試一下。
method1和method2可以依次輸出,說明了在不同的方法中也是可重入的。
(3)、不同的類方法是不是可重入的
既然是不同的類,那麼我們就在這裡定義兩個類,一個是Father,一個是Son。我們讓son調用father中的方法。
public class Father{ public synchronized void father() { System.out.println("父親"); } } class Son extends Father{ public static void main(String[] args) { Son instance1 = new Son(); instance1.son(); } public synchronized void son() { System.out.println("兒子"); super.father(); } }
在這裡son類中使用super.father()調用了父類中的synchronized方法,我們測試一下看看輸出結果:
2、不可中斷性質
不可中斷的意思你可以這樣理解,別人正在打遊戲,你也想玩,你必須要等別人不想玩了你才能去。在java中表示一旦這個鎖被別人搶走了,你必須等待。等別的線程釋放了鎖,你才可以拿到。否則就一直等下去。
這一點看起來是個有點但其實在某些場景下弊端超級大,因為假如拿到鎖得線程永遠的不釋放,那你就要永遠的等下去。
五、底層原理
對於原理,最好的方式就是深入到JVM中去。我們可以編譯看看其字節碼文件,再來分析,因此在這裡舉一個最簡單的例子。
1、定義一個簡單例子
public class SynTest11 { private Object object = new Object(); public void test() { synchronized(object){ System.out.println("java的架構師技術棧"); } } }
2、分析
分析的步驟很簡單,我們通過反編譯字節碼文件。記住我們的類名是SynTest11。
先編譯生成字節碼文件。
然後,我們再反編譯字節碼文件。
以上我們知道其是就是設置了一個監控器monitor。線程進來那就是monitorenter,線程離開是monitorexit。這就是synchronized關鍵字最基本的原理。
3、可重入原理
在上面我們曾提到可重入的性質,那麼synchronized關鍵字是如何保證的呢?其是工作是由我們的jvm來完成的,線程第一次給對象加鎖的時候,計數為1,以後這個線程再次獲取鎖的時候,計數會依次增加。同理,任務離開的時候,相應的計數器也會減少。
4、從java內存模型分析
java內存模型不是真正存在的,但是我們可以給出一個內存模型。synchronized關鍵字,會對同步的代碼會先寫到工作內存,等synchronized修飾的代碼塊一結束,就會寫入到主內存,這樣保證了同步。
六、缺陷
synchronized關鍵字既有優點也有缺點,而且缺點賊多,所以後來出現了比他更好的鎖。下面我們就來分析一下,這也是面試常問問題。
1、效率低
我們之前曾經分析過synchronized關鍵字是不可中斷的,這也就意味著一個等待的線程如果不能獲取到鎖將會一直等待,而不能再去做其他的事了。
這裡也說明了對synchronized關鍵字的一個改進措施,那就是設置超時時間,如果一個線程長時間拿不到鎖,就可以去做其他事情了。
2、不夠靈活
加鎖和解鎖的時候,每個鎖只能有一個對象處理,這對於目前分佈式等思想格格不入。
3、無法知道是否成功獲取到鎖
也就是我們的鎖如果獲取到了,我們無法得知。既然無法得知我們也就很不容易進行改進。
既然synchronized有這麼多缺陷。所以才出現了各種各樣的鎖。
七、總結
終於寫完了,synchronized涉及到的知識點,以及能夠引出來的知識點超級多,不過只有理解synchronized關鍵字,我們才可以更加深入的學習。本篇文章不可能面面俱到,只能說列出來一些常見的知識點。更加深入的理解我也會在後續的文章中指出。感謝大家的支持。