一、為什麼要用synchronized關鍵字 首先多線程中多個線程運行面臨共用數據同步的問題。 多線程正常使用共用數據時需要經過以下步驟: 1.線程A從共用數據區中複製出數據副本,然後處理。 2.線程A將處理好的數據副本寫入共用數據區。 3.線程B從共用數據區中複製出數據副本。 如此迴圈,直到線程結 ...
一、為什麼要用synchronized關鍵字
首先多線程中多個線程運行面臨共用數據同步的問題。
多線程正常使用共用數據時需要經過以下步驟:
1.線程A從共用數據區中複製出數據副本,然後處理。
2.線程A將處理好的數據副本寫入共用數據區。
3.線程B從共用數據區中複製出數據副本。
如此迴圈,直到線程結束。
假如線程A從共用數據區中複製出數據副本然後處理,在還沒有將更新的數據放入主記憶體時,線程B來到主記憶體讀取了未更新的數據,這樣就出問題了。
這就是所謂的臟讀,這類問題稱為多線程的併發問題。
舉個具體的例子:
1 public class TestThread { 2 public static void main(String[] args){ 3 TestSynchronized s = new TestSynchronized(); 4 new Thread(s,"t1").start(); //兩個線程訪問一個對象 5 new Thread(s,"t2").start(); 6 } 7 } 8 9 class TestSynchronized implements Runnable{ 10 private int ticket = 5; 11 12 public void run(){ 13 for(int p = 0; p < 10; p++){ 14 try { 15 Thread.sleep(500); 16 } catch (InterruptedException e) { 17 // TODO Auto-generated catch block 18 e.printStackTrace(); 19 } 20 if(ticket >= 0){ 21 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 22 } 23 } 24 } 25 }
運行結果: t2 ticket:4 t1 ticket:5 t1 ticket:2 t2 ticket:3 t1 ticket:1 t2 ticket:1 t2 ticket:0
可以看到1號票同時給了t1和t2,當t1讀入1執行了ticket--後,數據還沒有來得及寫入主記憶體就被t2從主記憶體中讀走了1,就造成了這種現象。
要想避免這種現象就需要使用synchronized關鍵字,synchronized英譯為同步,我們認為暫且把他看做鎖定更好理解。
接下來我們看看synchronized如何使用。
二、synchronized的用法
1. synchronized修飾方法(也稱同步方法)
(1) java中每個對象都有一個鎖(lock),或者叫做監視器,當前線程訪問某個對象中synchronized修飾的方法(同步塊)時,線程需要獲取到該對象的鎖,獲取對象鎖後才能訪問該對象中synchronized方法(同步塊),且一個對象中只有一個鎖。
(2) 沒有獲得該對象的鎖的其他線程,無法訪問該對象中synchronized修飾的方法(同步塊)。
(3) 其他線程要想訪問該對象中synchronized修飾的方法需要獲取該對象的鎖。
(4) 對象鎖只有將synchronized方法(同步塊)中的內容運行完畢或遇到異常才會釋放鎖。
例一:
1 public class TestThread { 2 public static void main(String[] args){ 3 TestSynchronized s = new TestSynchronized(); 4 new Thread(s,"t1").start(); //兩個線程訪問一個對象 5 new Thread(s,"t2").start(); 6 } 7 } 8 9 class TestSynchronized implements Runnable{ 10 private int ticket = 5; 11 12 synchronized public void run(){ 13 for(int p = 0; p < 10; p++){ 14 try { 15 Thread.sleep(500); 16 } catch (InterruptedException e) { 17 // TODO Auto-generated catch block 18 e.printStackTrace(); 19 } 20 if(ticket >= 0){ 21 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 22 } 23 } 24 } 25 }
運行結果: t1 ticket:5 t1 ticket:4 t1 ticket:3 t1 ticket:2 t1 ticket:1 t1 ticket:0
我們來分析上面程式,首先線程t1進去run方法獲得對象s的鎖,然後執行完run方法釋放鎖,run運行忘了也就沒有t2的事了。
因為只有將synchronized修飾的方法執行完才會釋放鎖,故列印五個t1.。
還有一點,如果一個對象裡面有多個synchronized方法,某一時刻只能有一個線程進入其中一個synchronized修飾的方法,則這時其他任何線程無法進入該對象中任何一個synchronized修飾的方法。
補充片段:
1 public class TestThread { 2 public static void main(String[] args){ 3 Test m1 = new Test(); //兩個線程共訪問一個對象。 4 5 TestSynchronized_1 s1 = new TestSynchronized_1(m1); 6 TestSynchronized_2 s2 = new TestSynchronized_2(m1); 7 new Thread(s1,"t1").start(); 8 new Thread(s2,"t2").start(); 9 } 10 } 11 12 class Test{ 13 synchronized public void test1(){ 14 for(int p = 0; p < 5; p++){ 15 System.out.println("s1.run.TestSynchronized_test 1"); 16 } 17 } 18 19 synchronized public void test2(){ 20 for(int p = 0; p < 5; p++){ 21 System.out.println("s2.run.TestSynchronized_test 2"); 22 } 23 } 24 } 25 26 class TestSynchronized_1 implements Runnable{ 27 28 private Test m; 29 public TestSynchronized_1(Test m){ 30 this.m = m; 31 } 32 33 public void run(){ 34 m.test1(); 35 } 36 } 37 38 class TestSynchronized_2 implements Runnable{ 39 40 private Test m; 41 public TestSynchronized_2(Test m){ 42 this.m = m; 43 } 44 45 public void run(){ 46 m.test2(); 47 } 48 }
運行結果: s1.run.TestSynchronized_test 1 s1.run.TestSynchronized_test 1 s1.run.TestSynchronized_test 1 s1.run.TestSynchronized_test 1 s1.run.TestSynchronized_test 1 s2.run.TestSynchronized_test 2 s2.run.TestSynchronized_test 2 s2.run.TestSynchronized_test 2 s2.run.TestSynchronized_test 2 s2.run.TestSynchronized_test 2
當線程t1運行synchronized修飾的test1方法時,線程t2是無法運行test2方法。結合之前說的,一個對象鎖只有一把,而這裡是兩個線程共用對象(m1),當線程t1獲得鎖時,線程t2就只能等待。歸根結底把握幾個要點:
1.鎖的唯一性(一個對象只有一把鎖,但不同對象就有不同的鎖)
2.沒鎖不能進去入synchronized修飾內容中運行。
3.只有運行完synchronized修飾的內容或遇到異常才釋放鎖。
我們來看下麵這個代碼:
例二:
1 public class TestThread { 2 public static void main(String[] args){ 3 Mouth m1 = new Mouth(); 4 Mouth m2 = new Mouth(); 5 TestSynchronized s1 = new TestSynchronized(m1);//兩個線程訪問兩個對象。 6 TestSynchronized s2 = new TestSynchronized(m2); 7 new Thread(s1,"t1").start(); //線程t1 8 new Thread(s2,"t2").start(); //線程t2 9 } 10 } 11 12 class Mouth{ //資源及方法 13 synchronized public void test(){ 14 int ticket = 5; 15 for(int p = 0; p < 10; p++){ 16 try { 17 Thread.sleep(500); 18 } catch (InterruptedException e) { 19 // TODO Auto-generated catch block 20 e.printStackTrace(); 21 } 22 if(ticket >= 0){ 23 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 24 } 25 } 26 } 27 28 } 29 30 class TestSynchronized implements Runnable{ 31 private Mouth m = new Mouth(); 32 33 public TestSynchronized(Mouth m){ 34 this.m = m; 35 } 36 37 synchronized public void run(){ 38 m.test(); 39 } 40 }
運行結果: t1 ticket:5 t2 ticket:5 t2 ticket:4 t1 ticket:4 t1 ticket:3 t2 ticket:3 t2 ticket:2 t1 ticket:2 t1 ticket:1 t2 ticket:1 t2 ticket:0 t1 ticket:0
可以發現好像用synchronized修飾的test方法沒有起作用,怎麼是t1,t2怎麼是交替運行的?
我們回顧下之前說的對象鎖,線程獲得對象鎖後可以訪問該對象裡面synchronized修飾的方法,其他線程無法訪問。
我們上面的代碼裡面對象有兩個,一個是m1、一個是m2。
t1獲得了對象m1的鎖,然後訪問m1中的test方法;t2獲得了對象m2的鎖,然後訪問s2中的test方法。
線程t1和線程t2訪問的是不同的資源(m1,m2),並不相互干擾所以沒有影響。例一中是因為兩個線程訪問同一個資源(s1)所以synchronized的起了限製作用。
synchronized修飾方法時只能對多個線程訪問同一資源(對象)時起限製作用。
可能大家會說了,那我們有沒有辦法也限制下這種情況呢,答案當然是可以的。
這就是下麵要說的:
2.synchronized修飾靜態方法
當修飾靜態方法時鎖定的是類,而不是對象,我們先把例二修改下看下結果。
例三:
1 public class TestThread { 2 public static void main(String[] args){ 3 Mouth m1 = new Mouth(); 4 Mouth m2 = new Mouth(); 5 TestSynchronized s1 = new TestSynchronized(m1); 6 TestSynchronized s2 = new TestSynchronized(m2); //兩個線程訪問兩個對象 7 new Thread(s1,"t1").start(); 8 new Thread(s2,"t2").start(); 9 } 10 } 11 12 class Mouth{ 13 synchronized public static void test(){ //改為靜態方法,鎖定的是類。 14 int ticket = 5; 15 for(int p = 0; p < 10; p++){ 16 try { 17 Thread.sleep(500); 18 } catch (InterruptedException e) { 19 // TODO Auto-generated catch block 20 e.printStackTrace(); 21 } 22 if(ticket >= 0){ 23 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 24 } 25 } 26 } 27 28 } 29 30 class TestSynchronized implements Runnable{ 31 private Mouth m = new Mouth(); 32 33 public TestSynchronized(Mouth m){ 34 this.m = m; 35 } 36 37 synchronized public void run(){ 38 m.test(); 39 } 40 }
運行結果: t1 ticket:5 t1 ticket:4 t1 ticket:3 t1 ticket:2 t1 ticket:1 t1 ticket:0 t2 ticket:5 t2 ticket:4 t2 ticket:3 t2 ticket:2 t2 ticket:1 t2 ticket:0
當synchronized修飾靜態方法時,線程需要獲得類(Mouth)鎖才能運行,沒有獲得類鎖的線程無法運行,且獲得類鎖的線程會將synchronized修飾的靜態方法會運行完畢才釋放類鎖。
例如例三中的代碼,t1先獲得類(Mouth)鎖運行Mouth類中的test方法,而t2沒有類(Mouth)鎖就無法運行。一個類只有一個類鎖,卻可以有多個對象(t1,t2等...)都是一個類(Mouth)中的對象,只要一個線程獲取了類(Mouth)鎖,其他線程就要等到類鎖被釋放,然後獲得類(Mouth)鎖之後才能運行類(Mouth)中synchronized修飾的靜態方法。所以即使是兩個線程(t1,t2)訪問兩個不同的資源(m1,m2)也會受到限制,因為m1,m2都屬於一個類(Mouth),而鎖住類(Mouth)後每次只能有一個線程訪問該類(Mouth)中的sychronized修飾的靜態方法。
當t1訪問m1中的test時,首先獲得類(Mouth)鎖,這時如果t2訪問m2中的test方法時也需要獲得類鎖,可是這時類鎖已經被線程t1獲得,故t2無法訪問m2中的方法。只有等t1運行完方法中的內容或異常釋放鎖後t2才有機會獲得鎖,獲得鎖後才能運行。
而之前例一中t1,t2鎖的是對象,需要結合這幾段代碼理解下。
3.synchronized塊(也稱同步塊)
如果每次都鎖定的範圍都是一個方法,每次只能有一個線程進去勢必會導致效率的低下,這主要是鎖定範圍過多引起的。
這時可以根據實際情況鎖定合適的區域,這就要用到同步塊了
synchronized(需要鎖住的對象或類){ 鎖定的部分,需要鎖才能運行。 }
()中可以確定鎖定的是對象還是類,鎖定對象的話可以用this,對類上鎖類名加class,例如要鎖定Mounth類(Moutn.class)。
我們首先看個沒有任何同步的例子:
1 public class TestThread { 2 public static void main(String[] args){ 3 TestSynchronized s1 = new TestSynchronized(); 4 5 new Thread(s1,"t1").start(); //兩個線程訪問一個對象 6 new Thread(s1,"t2").start(); 7 } 8 } 9 10 class TestSynchronized implements Runnable{ 11 private int ticket = 5; 12 13 public void run(){ 14 for(int p = 0; p < 10; p++){ 15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 // TODO Auto-generated catch block 19 e.printStackTrace(); 20 } 21 if(ticket >= 0){ 22 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 23 } 24 } 25 } 26 }
運行結果: t1 ticket:5 t2 ticket:4 t2 ticket:3 t1 ticket:2 t2 ticket:1 t1 ticket:1 t2 ticket:0 t1 ticket:-1
其中出現了-1,我們在其中一塊區域加上同步形成同步塊。
1 public class TestThread { 2 public static void main(String[] args){ 3 TestSynchronized s1 = new TestSynchronized(); //兩個線程訪問一個對象 4 5 new Thread(s1,"t1").start(); 6 new Thread(s1,"t2").start(); 7 } 8 } 9 10 class TestSynchronized implements Runnable{ 11 private int ticket = 5; 12 13 public void run(){ 14 for(int p = 0; p < 10; p++){ 15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 // TODO Auto-generated catch block 19 e.printStackTrace(); 20 } 21 synchronized(this){ //此次加上同步塊,這部分內容一次只有一個線程可以進入,其他內容不受約束。 22 if(ticket >= 0){ //這裡鎖的是對象,這裡面的內容需要對象鎖才能運行。 23 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 24 } 25 } 26 } 27 } 28 }
運行結果: t2 ticket:5 t1 ticket:4 t1 ticket:3 t2 ticket:2 t1 ticket:1 t2 ticket:0
一次只能有一個線程進入同步塊中,就不會出現線程讀了未更新的數據或者多減一次的情況。未加synchronized修飾的其他區域不受影響,故兩個線程的順序不定。
下麵我們來看一個同步塊鎖定類的例子,效果和例三一樣,只不過例三使用靜態方法鎖定了類,而下麵這個是使用同步塊鎖定了類。
1 public class TestThread { 2 public static void main(String[] args){ 3 TestSynchronized s1 = new TestSynchronized(); 4 TestSynchronized s2 = new TestSynchronized(); //兩個線程訪問兩個對象 5 new Thread(s1,"t1").start(); 6 new Thread(s2,"t2").start(); 7 } 8 } 9 10 class TestSynchronized implements Runnable{ 11 private int ticket = 5; 12 13 synchronized public void run(){ 14 synchronized(TestSynchronized.class){ //將synchronized修飾的靜態方法改成了同步塊。 15 16 for(int p = 0; p < 10; p++){ 17 try { 18 Thread.sleep(500); 19 } catch (InterruptedException e) { 20 // TODO Auto-generated catch block 21 e.printStackTrace(); 22 } 23 if(ticket >= 0){ 24 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--); 25 } 26 } 27 } 28 } 29 }
運行結果: t1 ticket:5 t1 ticket:4 t1 ticket:3 t1 ticket:2 t1 ticket:1 t1 ticket:0 t2 ticket:5 t2 ticket:4 t2 ticket:3 t2 ticket:2 t2 ticket:1 t2 ticket:0
上述代碼和例三功能一樣,只是鎖定方法不同,這裡只是做下演示。
synchronized修飾方法是一種粗顆粒的併發控制,某一時刻只有一個線程執行方法內的內容效率較低下。
synchronized同步塊是一種細顆粒的併發控制,可以自行根據需求確定區域較為靈活,可以平衡下效率和安全,同時也能因選擇區域不恰當而造成問題。
只要不在synchronized方法(同步塊)內的其他部分都不受限制。
普通方法鎖定的對象,需要獲得對象鎖
靜態方法鎖定的是類,需要獲得類鎖。
同步塊可以確定是鎖對象(this )還是鎖類(xxx.class),同時也可以自行確定區域。