java多執行緒3:synchronized
- 2021 年 12 月 13 日
- 筆記
執行緒安全
多個執行緒共同訪問一個對象的實例變數,那麼就可能出現執行緒不安全的問題。
先看一段程式碼示例,定義一個對象 MyDomain1
public class MyDomain1 { private int num = 0; public void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
寫兩個執行緒分別去add字元串”a”和字元串”b”
public class Mythread1_1 extends Thread { private MyDomain1 numRef; public Mythread1_1(MyDomain1 numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("a"); } }
@Test public void test() throws InterruptedException { MyDomain1 mythread1 = new MyDomain1(); Mythread1_1 athread = new Mythread1_1(mythread1); athread.start(); Mythread1_2 bthread = new Mythread1_2(mythread1); bthread.start(); athread.join(); bthread.join(); System.out.println(Thread.currentThread().getName()); }
執行結果:
a set over! b set over! b num=200 a num=200 main
按照正常來看應該列印”a num = 100″和”b num = 200″才對,現在卻列印了”b num = 200″和”a num = 200″,這就是執行緒安全問題。
當我們給 MyDomain1的 addI方法加上同步 synchronized 後,
執行結果為:
a set over! a num=100 b set over! b num=200 main
多個執行緒實例,訪問同一個共享實例變數,非執行緒安全問題:多個執行緒對同一個對象中的同一個實例變數操作時 ,Mythread1 方法用synchronized修飾 可以解決執行緒非安全問題。
多個對象多個鎖
@Test public void test1() throws InterruptedException { MyDomain1 mythread1 = new MyDomain1(); Mythread1_1 athread = new Mythread1_1(mythread1); athread.start(); MyDomain1 mythread2 = new MyDomain1(); Mythread1_2 bthread = new Mythread1_2(mythread2); bthread.start(); athread.join(); bthread.join(); System.out.println(Thread.currentThread().getName()); }
運行結果:
a set over! b set over! b num=200 a num=100 main
第6行,我們再定義一個新對象,當同時執行 athread 和 bthread的時候,列印的順序是交叉的。
關鍵字synchronized取得的鎖都是對象鎖,而不是把一段程式碼或方法(函數)當作鎖,
所以在上面的示例中,哪個執行緒先執行帶synchronized關鍵字的方法,哪個執行緒就持有該方法所屬對象的鎖Lock,
那麼其他執行緒只能呈等待狀態,前提是多個執行緒訪問的是同一個對象。但如果多個執行緒訪問多個對象,則JVM會創建多個鎖。
上面的示例就是創建了2個MyDomain1.java類的對象,所以就會產生出2個鎖,因此兩個執行緒之間不會受到對方加鎖的約束。
synchronized方法與鎖對象
在一個實體類, 定義一個同步方法和一個非同步方法:
public class MyDomain2 {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName=" + Thread.currentThread().getName()+ " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 也加上synchronized修飾
public void methodB() {
try {
System.out.println("begin methodB threadName=" + Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然後定義兩個執行緒類,一個調用同步方法,一個調用非同步方法
public class Mythread2_1 extends Thread { private MyDomain2 object; public Mythread2_1(MyDomain2 object) { this.object = object; } @Override public void run() { object.methodA(); } }
public class Mythread2_2 extends Thread { private MyDomain2 object; public Mythread2_2(MyDomain2 object) { this.object = object; } @Override public void run() { object.methodB(); } }
@Test public void test2() throws InterruptedException { MyDomain2 object = new MyDomain2(); Mythread2_1 a = new Mythread2_1(object); a.setName("A"); Mythread2_2 b = new Mythread2_2(object); b.setName("B"); a.start(); b.start(); a.join(); b.join(); }
運行結果:
begin methodA threadName=A begin time=1639384569539 begin methodB threadName=B begin time=1639384569560 end endTime=1639384574541 end endTime=1639384574564
可以看到methodA和methodB基本同時執行,當我們把methodB也加上 synchronized 修飾後:
begin methodA threadName=A begin time=1639384695430 end endTime=1639384700433 begin methodB threadName=B begin time=1639384700433 end endTime=1639384705437
可以看出methodA執行完之後,methodB方法才開始執行。
因此我們可以得出結論:
1、A執行緒持有Object對象的Lock鎖,B執行緒可以以非同步方式調用Object對象中的非synchronized類型的方法
2、A執行緒持有Object對象的Lock鎖,B執行緒如果在這時調用Object對象中的synchronized類型的方法則需要等待,也就是同步
鎖重入
關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個執行緒得到一個對象鎖後,再次請求此對象鎖時是可以再次得到該對象的鎖的。
這也證明在一個synchronized方法/塊的內部調用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。
定義三個同步方法,它們之間順序調用:
public class MyDomain3 { synchronized public void service1() { System.out.println("service1"); service2(); } synchronized public void service2() { System.out.println("service2"); service3(); } synchronized public void service3() { System.out.println("service3"); } }
public class Mythread3 extends Thread { public void run() { MyDomain3 service = new MyDomain3(); service.service1(); } }
@Test public void test3() throws InterruptedException { Mythread3 t = new Mythread3(); t.start(); t.join(); }
執行結果:
service1 service2 service3
「可重入鎖」的概念是:自己可以再次獲取自己的內部鎖。
比如有1條執行緒獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。
定義子父類:
public class MyDomain3_1_Father { public int i = 10; synchronized public void operateIMainMethod() { try { i--; System.out.println("main print i=" + i); Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
public class MyDomain3_1_Son extends MyDomain3_1_Father { synchronized public void operateISubMethod() { try { while (i > 0) { i--; System.out.println("sub print i=" + i); Thread.sleep(100); this.operateIMainMethod(); } } catch (InterruptedException e) { e.printStackTrace(); } } }
執行緒類執行子類方法
public class Mythread3_1 extends Thread { @Override public void run() { MyDomain3_1_Son sub = new MyDomain3_1_Son(); sub.operateISubMethod(); } }
@Test public void test3() throws InterruptedException { // 子類完全可以通過可重入鎖調用父類的同步方法 Mythread3_1 t = new Mythread3_1(); t.start(); t.join(); }
執行結果:
sub print i=9 main print i=8 sub print i=7 main print i=6 sub print i=5 main print i=4 sub print i=3 main print i=2 sub print i=1 main print i=0
當存在父子類繼承關係時,子類是完全可以通過「可重入鎖」調用父類的同步方法的。
異常自動釋放鎖
當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放。
模擬的是把一個long型數作為除數,從MAX_VALUE開始遞減,直至減為0,從而產生ArithmeticException。看一下例子:
public class MyDomain4 { synchronized public void testMethod() { try { System.out.println(Thread.currentThread().getName() + "進入synchronized方法"); long l = Integer.MAX_VALUE; while (true) { long lo = 2 / l; l--; } } catch (Exception e) { e.printStackTrace(); } } }
public class Mythread4 extends Thread { private MyDomain4 service; public Mythread4(MyDomain4 service) { this.service = service; } public void run() { service.testMethod(); } }
@Test public void test4() throws InterruptedException { MyDomain4 myDomain4 = new MyDomain4(); Mythread4 a = new Mythread4(myDomain4); Mythread4 b = new Mythread4(myDomain4); a.start(); b.start(); a.join(); b.join(); }
執行結果:
Thread-0進入synchronized方法 java.lang.ArithmeticException: / by zero at multithreading.synchronizedDemo.MyDomain4.testMethod(MyDomain4.java:15) at multithreading.synchronizedDemo.Mythread4.run(Mythread4.java:17) Thread-1進入synchronized方法 java.lang.ArithmeticException: / by zero at multithreading.synchronizedDemo.MyDomain4.testMethod(MyDomain4.java:15) at multithreading.synchronizedDemo.Mythread4.run(Mythread4.java:17)
加鎖方式
synchronized 的不同加鎖方式有不同是阻塞效果
1:synchronized (this)
在實體類中定義兩個同步方法
public class MyDomain8 { public void serviceMethodA() { synchronized (this) { try { System.out.println("A begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("A end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } public void serviceMethodB() { synchronized (this) { System.out.println("B begin time = " + System.currentTimeMillis()); System.out.println("B end time = " + System.currentTimeMillis()); } } }
調用同步方法B
public class MyThread8_1 extends Thread { private MyDomain8 td; public MyThread8_1(MyDomain8 td) { this.td = td; } public void run() { td.serviceMethodB(); } }
調用同步方法A
public class MyThread8_2 extends Thread { private MyDomain8 td; public MyThread8_2(MyDomain8 td) { this.td = td; } public void run() { td.serviceMethodA(); } }
@Test public void test8() throws InterruptedException { MyDomain8 td = new MyDomain8(); MyThread8_1 a = new MyThread8_1(td); MyThread8_2 b = new MyThread8_2(td); a.start(); b.start(); a.join(); b.join(); }
執行結果:
B begin time = 1639386305338 B end time = 1639386305338 A begin time = 1639386305339 A end time = 1639386307339
synchronized(this)塊 獲得的是一個對象鎖,換句話說,synchronized塊鎖定的是整個對象。
2:synchronized 非靜態方法
public class MyDomain9 { public void serviceMethodA() { synchronized (this) { try { System.out.println("A begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("A end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public void serviceMethodB() { System.out.println("B begin time = " + System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("B end time = " + System.currentTimeMillis()); } }
@Test public void test9() throws InterruptedException { MyDomain9 td = new MyDomain9(); MyThread9_1 a = new MyThread9_1(td); MyThread9_2 b = new MyThread9_2(td); a.start(); b.start(); a.join(); b.join(); }
執行結果:
B begin time = 1639386457971 B end time = 1639386459972 A begin time = 1639386459974 A end time = 1639386461974
結論:(1)對其他synchronized同步方法或synchronized(this)同步程式碼塊呈阻塞狀態
(2)同一時間只有一個執行緒可以執行synchronized同步方法中的程式碼
3:synchronized(非this非自身其他對象x)
定義一個非自身對象的鎖
public class MyDomain10 { private String anyString = new String(); public void serviceMethodA() { synchronized (anyString) { try { System.out.println("A begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("A end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public void serviceMethodB() { try { System.out.println("B begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("B end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }
@Test public void test10() throws InterruptedException { MyDomain10 td = new MyDomain10(); MyThread10_1 a = new MyThread10_1(td); MyThread10_2 b = new MyThread10_2(td); a.start(); b.start(); a.join(); b.join(); }
執行結果:
B begin time = 1639387076574 A begin time = 1639387076575 B end time = 1639387078577 A end time = 1639387078578
兩個方法幾乎同時執行,同時結束,說明 synchronized(非this非自身其他對象x)程式碼塊與synchronized方法呈非阻塞狀態。
4:synchronized(非this自身對象x)
定義一個分別需要自身對象的同步方法A和 非靜態同步方法B
public class MyDomain11 { public void serviceMethodA(MyDomain11 anyString) { synchronized (anyString) { try { System.out.println("A begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("A end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public void serviceMethodB() { try { System.out.println("B begin time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("B end time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } }
@Test public void test11() throws InterruptedException { MyDomain11 td = new MyDomain11(); MyThread11_1 a = new MyThread11_1(td); MyThread11_2 b = new MyThread11_2(td); a.start(); b.start(); a.join(); b.join(); }
執行結果:
B begin time = 1639387213624 B end time = 1639387215625 A begin time = 1639387215625 A end time = 1639387217628
可見 synchronized(非this自身對象x)程式碼塊與synchronized方法或synchronized(this)都呈阻塞狀態,
這兩個測試方法,也能證明 synchronized方法與synchronized(this) 持有的都是對象鎖。
5:synchronized靜態方法
代表的是對當前.java文件對應的Class類加鎖
public class MyDomain12 { public synchronized static void printA() { try { System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA()方法"); Thread.sleep(3000); System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA()方法"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void printB() { // synchronized靜態方法持有的是對當前.java文件對應的Class類加鎖 synchronized (MyDomain12.class) { System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB()方法"); System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB()方法"); } } public synchronized void printC() { System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printC()方法"); System.out.println( "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printC()方法"); } }
@Test public void test12() throws InterruptedException { MyDomain12 md = new MyDomain12(); Mythread12_1 mt1 = new Mythread12_1(); Mythread12_2 mt2 = new Mythread12_2(); Mythread12_3 mt3 = new Mythread12_3(md); mt1.start(); mt2.start(); mt3.start(); mt1.join(); mt2.join(); mt3.join(); }
執行結果:
執行緒名稱為:Thread-0在1639387516115進入printA()方法 執行緒名稱為:Thread-2在1639387516116進入printC()方法 執行緒名稱為:Thread-2在1639387516116離開printC()方法 執行緒名稱為:Thread-0在1639387519119離開printA()方法 執行緒名稱為:Thread-1在1639387519120進入printB()方法 執行緒名稱為:Thread-1在1639387519120離開printB()方法
結論:synchronized靜態方法也是互斥的(printA和printB可以看出)
synchronized靜態方法與synchronized方法持有的是不同的鎖(printC()方法的調用和對printA()方法、printB()方法的調用時非同步的)
synchronized靜態方法持有的是對當前.java文件對應的Class類加鎖
參考文獻
1:《Java並發編程的藝術》
2:《Java多執行緒編程核心技術》