一、目錄
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<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(()->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(()->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(()->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(()->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、馬士兵並發編程、並發編程實踐
