併發編程(一):從頭到腳解讀synchronized

1、目錄

  一、多線程啓動方式java

  二、synchronized的基本用法編程

  三、深度解析synchronized小程序

  四、同步方法與非同步方法是否能同時調用?緩存

  五、同步鎖是否可重入(可重入鎖)?多線程

  六、異常是否會致使鎖釋放?併發

  七、鎖定某對象,對象屬性改變是否會影響鎖?指定其餘對象是否會影響鎖?app

  八、synchronized編程建議ide

2、多線程啓動方式

繼承Thread重寫run()或者實現Runnable接口。測試

 1 //實現runnable接口
 2     static class MyThread implements Runnable{  3  @Override  4         public void run() {  5             
 6  }  7  }  8     
 9     //繼承Thread+重寫run
10     static class MThread extends Thread{ 11  @Override 12         public void run() { 13             super.run(); 14  } 15  } 16     
17     //測試方式
18     public static void main(String[] args) { 19         new Thread(new MyThread(),"t").start(); 20         new MThread().start(); 21     }

2、synchronized的基本用法

一、實例變量對象做爲鎖對象優化

/** * synchronized 鎖對象 * @author qiuyongAaron */
public class T1 { private int count=10; //利用Object實例對象標記互斥鎖,每一個線程進行同步代碼塊的時候,須要先去堆內存object獲取鎖標記,只有沒有被其它線程標記的時候才能得到鎖標記。
     Object object =new Object(); public void method(){ synchronized(object){ count++; System.out.println(Thread.currentThread().getName()+":count="+count); } } } /** *鎖定當前對象,原理跟上面同樣,只是談一下應用狀況。 *@author qiuyongAaron */
public class T2 { private int count=10; public void method(){ synchronized(this){ count++; System.out.println(Thread.currentThread().getName()+":count="+count); } } //該種書寫方式等價於上面的method
     public synchronized void cloneMethod(){ count++; System.out.println(Thread.currentThread().getName()+":count="+count); } }

總結:synchronized不是鎖定代碼塊,它是在訪問某段代碼塊的時候,去尋找鎖定對象上的標記(實質上就是一個變量增減,這就是這個標記)。以T2爲例,T2對象爲鎖定對象,假設開啓5個線程,線程A最早競爭到鎖,那麼線程A在T2對象上進行標記,至關於標記變量加1。就在這時,其餘4個線程競爭到鎖之後,發現T2對象標記變量不爲0,那麼他們就被阻塞,等待線程A釋放鎖的時候,標記變量會減1使它變爲0,其餘鎖就能競爭到鎖。虛擬機:發生就近原則-鎖定原則:釋放鎖先於得到鎖,簡而言之,只有線程A釋放鎖(鎖定對象標記變量爲0),其餘線程才能得到鎖(鎖定對象標記+1)。

 

二、靜態變量對象做爲鎖對象

/** * 鎖定靜態變量 * @author qiuyongAaron */
public class T3 { public static int count=10; public synchronized void method(){ count++; System.out.println(Thread.currentThread().getName()+":count="+count); } //等價於上述方法
     public static void cloneMethod(){ synchronized (T3.class) {//這裏寫this能夠嗎?
                count++; System.out.println(Thread.currentThread().getName()+":count="+count); } } }

問題:爲何靜態變量要寫T3.class,不能寫this?

回答:這須要瞭解反射與類加載過程才能透徹解析。類加載過程:類加載-->驗證-->準備-->解析-->初始化-->使用卸載,在類加載階段,將會把靜態變量、常量所有加載在堆內存的方法區中,而且會生成Class對象,T3.class就至關於Class對象,然而this是T3對象,而何時可以產生T3對象?當應用程序調用new T3()的構造器時候,也就是在初始化階段纔會產生。因此靜態變量做爲鎖定對象只能用T3.class,不能使用this對象。

總結:靜態變量在類加載的時候就存入內存,而實例變量是要調用構造器的時候才能加載進內存。因此,T3.class是類加載產生,this是初始化產生,天然標記鎖定對象的時候是用T3.class不用this。

3、深度解析synchronized

synchronized定義:互斥鎖,保證原子性、可見性。也就是,當線程A得到鎖,其餘線程所有被阻塞。以前解析過不過多贅述。

多線程不加鎖:

 1 //多線程不加鎖!
 2 public class T4 {  3      public static void main(String[] args) {  4            MyThread t=new MyThread();  5            Thread t1=new Thread(t,"t1");  6            Thread t2=new Thread(t,"t2");  7  t1.start();  8  t2.start();  9  } 10 
11      static class MyThread implements Runnable{ 12            private int value =0; 13  @Override 14            public void run() { 15 
16                 for(int i=0;i<5;i++){ 17                      value++; 18                      System.out.println(Thread.currentThread().getName()+":"+this.value); 19  } 20  } 21  } 22 } 23 
24 //運行結果:每次運行結果都不一樣
25 t1:2 t2:2 t1:3 t2:4 t1:5 t2:6 t1:7 t2:8 t1:9 t2:10

多線程加鎖:

//多線程加鎖!
public class T5 { public static void main(String[] args) { MyThread t=new MyThread(); Thread t1=new Thread(t,"t1"); Thread t2=new Thread(t,"t2"); t1.start(); t2.start(); } static class MyThread implements Runnable{ private int value =0; @Override public synchronized void run() { for(int i=0;i<5;i++){ value++; System.out.println(Thread.currentThread().getName()+":"+this.value); } } } } 運行結果: t1:1 t1:2 t1:3 t1:4 t1:5 t2:6 t2:7 t2:8 t2:9 t2:10

顯然,加了同步互斥鎖的例子程序符合咱們業務需求,那麼想一下這是爲何?

先談Java內存模型:

分析:在虛擬機中,堆內存用於存儲共享數據(實例對象),堆內存也就是這裏說的主內存。

   每一個線程將會在堆內存中開闢一塊空間叫作線程的工做內存,附帶一塊緩存區用於存儲共享數據副本。那麼,共享數據在堆內存當中,線程通訊就是經過主內存爲中介,線程在本地內存讀而且操做完共享變量操做完畢之後,把值寫入主內存。

 

分析程序1:

  • t1從主存中讀取共享變量value:0,而且執行完value++後value:1,寫入主存。
  • t2啓動讀取主存value:1到工做內存,執行並打印value爲2,3。
  • t2讀取的是它工做內存的值,因此這時t1的本地內存並無改變仍是1,執行打印輸入value:2。
  • 一樣邏輯執行...
  • 來看t2:六、t2:八、t1:七、t1:9,爲何?
  • 當t2在工做內存操做完共享變量,t2把共享變量爲value:6寫入主存。
  • 就在這時,t1從主存讀取共享變量value:6而且value++爲7,還沒來得及打印。
  • t2從主存讀取共享變量value:7,value++,打印value:8,而且寫入主存。
  • 這時,繼續以前的操做value++,天然打印的值仍是7,再讀取主存值value:8
  • 這時t1打印value:9,value:10。

 

分析程序2:

  • 在虛擬機的先行發生原則中(happen-before)的鎖定原則:對某一個對象加鎖的時候,它接鎖先於加鎖,意思就是必須等線程A鎖釋放,才能被線程B訪問。
  • 回到這個小程序,t1啓動、t2被阻塞不能訪問共享變量。以前,咱們談過java內存模型,假設線t1啓動讀取共享數據,而且會把共享數據寫入到工做內存的緩存中,t1在本地內存操做完,待它操做完不把數據寫回主存,這樣即使t2被堵塞也沒用?因此,虛擬機規定,線程unlock的時候必須把數據刷新到主存,lock的時候必須從主存刷新數據到工做內存。
  • 什麼意思?最開始主存共享變量value:0,t1得到同步鎖,t2被阻塞。t1操做value:1-5,假設t1在本地內存操做完就立刻釋放鎖並不把value寫入主存,這時t2得到同步鎖,從主存讀到的共享變量依然爲0,這虛擬機豈能容忍?因此,虛擬機規定,t1必須unlock以前把數據從線程工做內存刷新到主存,t2必須lock之後把數據從主存刷新到線程工做內存。

4、同步方法與非同步方法是否能同時調用?

 1 /**
 2  * 線程是否能夠同時調用同步方法與非同步方法?  3  * @author qiuyongAaron  4  */
 5 public class T6 {  6 
 7      public synchronized void m1() {  8            System.out.println(Thread.currentThread().getName() + " m1 start...");  9            try { 10                 Thread.sleep(10000); 11            } catch (InterruptedException e) { 12  e.printStackTrace(); 13  } 14            System.out.println(Thread.currentThread().getName() + " m1 end"); 15  } 16 
17      public void m2() { 18            try { 19                 Thread.sleep(5000); 20            } catch (InterruptedException e) { 21  e.printStackTrace(); 22  } 23            System.out.println(Thread.currentThread().getName() + " m2 "); 24  } 25 
26      public static void main(String[] args) { 27            T6 t = new T6(); 28            new Thread(()->t.m1(),"t1").start(); 29            new Thread(()->t.m2(),"t2").start(); 30  } 31 } 32 //運行結果:
33 t1:start!
34 t2:start!
35 t1:end!

 總結:顯然能夠,首先synchronized同步互斥鎖是鎖定對象,t1鎖定的T6對象。線程t1去訪問代碼塊t.m1()的時候會去申請鎖,去查看鎖定標記是否爲0,再決定是否阻塞。然而線程t2訪問t.m2()都不用申請鎖,因此你鎖定標記爲何,與我有什麼關係?因此,上述問題固然是成立!

5、同步互斥鎖是否可重入(可重入鎖)?

 1 /**
 2  * 當鎖定同一個對象的時候,鎖只是在對象添加標記,加鎖一次標記+1,解鎖一次標記-1,直到標記爲0釋放鎖。  3  * 可重入鎖  4  * @author qiuyongAaron  5  */
 6 public class T7 {  7      public synchronized void m1(){  8            try {  9                 Thread.sleep(5000); 10            } catch (Exception e) { 11  e.printStackTrace(); 12  } 13  m2(); 14  } 15 
16      public synchronized void m2(){ 17            try { 18                 Thread.sleep(5000); 19            } catch (Exception e) { 20  e.printStackTrace(); 21  } 22  } 23 }

總結:synchronized同步互斥鎖,支持可重入。在開篇咱們就談了,申請鎖意味着對鎖定對象的標記變量值修改,若是是同一個鎖定變量,那麼沒重入一次,鎖標記變量+1。若是想鎖釋放,那麼必須釋放鎖-1,直到標記變量爲0,鎖才能被釋放被其餘線程佔用。

6、異常是否會致使鎖釋放?

/** * 異常將致使鎖釋放! * @author qiuyongAaron */
public class T9 { public synchronized void m1(){ int i=0; System.out.println(Thread.currentThread().getName()+":start!"); while(true){ if(i==10){ System.out.println(5/0); } i++; } } public void m2(){ System.out.println(Thread.currentThread().getName()+":start!"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":end!"); } public static void main(String[] args) { T9 t=new T9(); new Thread(()->t.m1(),"t1").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->t.m2(),"t2").start(); } } 運行結果: t1:start! Exception in thread "t1" java.lang.ArithmeticException: / by zero at com.ccut.aaron.synchronize.T9.m1(T9.java:12) at com.ccut.aaron.synchronize.T9.lambda$0(T9.java:30) at java.lang.Thread.run(Thread.java:745) t2:start! t2:end!

總結:答案是產生異常將會釋放鎖,因此在編寫代碼時候須要處理異常。從例子程序可看出,若是不釋放鎖的話,t1一直佔用鎖,而t2不可能得到鎖。從運行結果看出,t2得到鎖資源,因此證實了原命題。

7、鎖定某對象,對象屬性改變是否會影響鎖?指定其餘對象是否會影響鎖?

/** * 鎖定對象改變屬性無影響,若是鎖定對象指定新對象,鎖定對象將會改變! * @author xiaoyongAaron */
public class T10 { Object o=new Object(); public void m(){ synchronized(o){ while(true){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } } } public static void main(String[] args) { T10 t=new T10(); new Thread(()->t.m(),"t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } t.o=new Object(); new Thread(()->t.m(),"t2").start(); } } 運行結果: t1 t1 t2

總結:從運行結果看出原命題的答案是,修改鎖定變量的屬性不會改變鎖,鎖定變量指定新對象將會報錯。看例子程序,假設鎖沒有轉移到新的實例變量,那麼t2將會一直被阻塞。

8、synchronized編程建議

一、儘可能鎖定有共享數據的代碼塊,這是併發編程的優化中的鎖粗化。

二、不要用常量做爲鎖定對象,由於常量池的常量同時被兩個地方引用將會產生很大的問題。

/** *鎖粗化 *@author qiuyongAaron */
public void T11{ int count=0; public synchronized void m(){ for(int i=0;i<10;i++){} System.out.println("hello world!"); synchronized(this){ count++; } } } /** *不要使用常量做爲鎖定對象!! *他們是同一個鎖定對象!! *@author qiuyongAaron */
public void T11{ String s1 = "Hello"; String s2 = "Hello"; void m1() { synchronized(s1) {} } void m2() { synchronized(s2) {} } }

 9、版權聲明

  做者:邱勇Aaron

  出處:http://www.cnblogs.com/qiuyong/

  您的支持是對博主深刻思考總結的最大鼓勵。

  本文版權歸做者全部,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,尊重做者的勞動成果。

  參考:深刻理解JVM、馬士兵併發編程、併發編程實踐

相關文章
相關標籤/搜索