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


一、目錄

  1、多線程啟動方式

  2、synchronized的基本用法

  3、深度解析synchronized

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

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

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

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

  8、synchronized編程建議

二、多線程啟動方式

繼承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     }

二、synchronized的基本用法

1、實例變量對象作為鎖對象

/** * 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;

 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> method(){
       </span><span style="color: #0000ff;">synchronized</span>(<span style="color: #0000ff;">this</span><span style="color: #000000;">){
            count</span>++<span style="color: #000000;">;
            System.out.println(Thread.currentThread().getName()</span>+":count="+<span style="color: #000000;">count);
       }
 }

 </span><span style="color: #008000;">//</span><span style="color: #008000;">該種書寫方式等價於上面的method</span>
 <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> cloneMethod(){
       count</span>++<span style="color: #000000;">;
      System.out.println(Thread.currentThread().getName()</span>+":count="+<span style="color: #000000;">count);
 }

}

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

 

2、靜態變量對象作為鎖對象

/** * 鎖定靜態變量 * @author qiuyongAaron */
public class T3 { public static int count=10; 
 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> method(){
       count</span>++<span style="color: #000000;">;
      System.out.println(Thread.currentThread().getName()</span>+":count="+<span style="color: #000000;">count);
 }

 </span><span style="color: #008000;">//</span><span style="color: #008000;">等價於上述方法</span>
 <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> cloneMethod(){
       </span><span style="color: #0000ff;">synchronized</span> (T3.<span style="color: #0000ff;">class</span>) {<span style="color: #008000;">//</span><span style="color: #008000;">這里寫this可以嗎?</span>
            count++<span style="color: #000000;">;
            System.out.println(Thread.currentThread().getName()</span>+":count="+<span style="color: #000000;">count);
       }
 }

}

問題:為什么靜態變量要寫T3.class,不能寫this?

回答:這需要了解反射與類加載過程才能透徹解析。類加載過程:類加載-->驗證-->准備-->解析-->初始化-->使用卸載,在類加載階段,將會把靜態變量、常量全部加載在堆內存的方法區中,並且會生成Class對象,T3.class就相當於Class對象,然而this是T3對象,而什么時候能夠產生T3對象?當應用程序調用new T3()的構造器時候,也就是在初始化階段才會產生。所以靜態變量作為鎖定對象只能用T3.class,不能使用this對象。

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

三、深度解析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(); } 
 </span><span style="color: #0000ff;">static</span> <span style="color: #0000ff;">class</span> MyThread <span style="color: #0000ff;">implements</span><span style="color: #000000;"> Runnable{
       </span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">int</span> value =0<span style="color: #000000;">;
       @Override
       </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {

            </span><span style="color: #0000ff;">for</span>(<span style="color: #0000ff;">int</span> i=0;i&lt;5;i++<span style="color: #000000;">){
                 value</span>++<span style="color: #000000;">;
                 System.out.println(Thread.currentThread().getName()</span>+":"+<span style="color: #0000ff;">this</span><span style="color: #000000;">.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:6、t2:8、t1:7、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以后把數據從主存刷新到線程工作內存。

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

 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()都不用申請鎖,所以你鎖定標記為什么,與我有什么關系?所以,上述問題當然是成立!

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

 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,鎖才能被釋放被其他線程占用。

六、異常是否會導致鎖釋放?

/** * 異常將導致鎖釋放! * @author qiuyongAaron */
public class T9 { 
 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> m1(){
       </span><span style="color: #0000ff;">int</span> i=0<span style="color: #000000;">;
      System.out.println(Thread.currentThread().getName()</span>+":start!"<span style="color: #000000;">);
       </span><span style="color: #0000ff;">while</span>(<span style="color: #0000ff;">true</span><span style="color: #000000;">){
            </span><span style="color: #0000ff;">if</span>(i==10<span style="color: #000000;">){
                 System.out.println(</span>5/0<span style="color: #000000;">);
            }
            i</span>++<span style="color: #000000;">;
       }
 }

 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> m2(){
      System.out.println(Thread.currentThread().getName()</span>+":start!"<span style="color: #000000;">);
       </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            Thread.sleep(</span>10000<span style="color: #000000;">);
       } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
       }
      System.out.println(Thread.currentThread().getName()</span>+":end!"<span style="color: #000000;">);
 }

 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
       T9 t</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> T9();
       </span><span style="color: #0000ff;">new</span> Thread(()-&gt;t.m1(),"t1"<span style="color: #000000;">).start();
       </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            TimeUnit.SECONDS.sleep(</span>2<span style="color: #000000;">);
       } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
       }
       </span><span style="color: #0000ff;">new</span> Thread(()-&gt;t.m2(),"t2"<span style="color: #000000;">).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獲得鎖資源,所以證明了原命題。

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

/** * 鎖定對象改變屬性無影響,如果鎖定對象指定新對象,鎖定對象將會改變! * @author xiaoyongAaron */
public class T10 { Object o=new Object(); 
 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> m(){
       </span><span style="color: #0000ff;">synchronized</span><span style="color: #000000;">(o){
            </span><span style="color: #0000ff;">while</span>(<span style="color: #0000ff;">true</span><span style="color: #000000;">){
                 </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
                       TimeUnit.SECONDS.sleep(</span>1<span style="color: #000000;">);
                 } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
                       e.printStackTrace();
                 }
                 System.out.println(Thread.currentThread().getName());
            }
       }
 }

 </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
       T10 t</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> T10();

       </span><span style="color: #0000ff;">new</span> Thread(()-&gt;t.m(),"t1"<span style="color: #000000;">).start();

       </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            TimeUnit.SECONDS.sleep(</span>1<span style="color: #000000;">);
       } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
       }

       t.o</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> Object();

       </span><span style="color: #0000ff;">new</span> Thread(()-&gt;t.m(),"t2"<span style="color: #000000;">).start();
 }

}
運行結果:
t1 t1 t2

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

八、synchronized編程建議

1、盡量鎖定有共享數據的代碼塊,這是並發編程的優化中的鎖粗化。

2、不要用常量作為鎖定對象,因為常量池的常量同時被兩個地方引用將會產生很大的問題。

/** *鎖粗化 *@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";

 </span><span style="color: #0000ff;">void</span><span style="color: #000000;"> m1() {
    </span><span style="color: #0000ff;">synchronized</span><span style="color: #000000;">(s1) {}
 }

 </span><span style="color: #0000ff;">void</span><span style="color: #000000;"> m2() {
     </span><span style="color: #0000ff;">synchronized</span><span style="color: #000000;">(s2) {}
 }

}

 九、版權聲明

  作者:邱勇Aaron

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

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

  本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,尊重作者的勞動成果。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM