一、Synchronized(this)鎖代碼塊
用關鍵字synchronized修飾方法在有些情況下是有弊端的,若是執行該方法所需的時間比較長,線程1執行該方法的時候,線程2就必須等待。這種情況下就可以使用synchronized同步該方法中會引起線程安全的那部分代碼,其余不會引起線程安全的就不需要同步,這部分代碼就可以多線程並發執行,減少時間提高效率。
舉例:多線程執行同一個方法時,同步方法和同步代碼塊花費時間的比較
1、synchronized修飾方法(同步方法)
synchronized修飾longTimeTask方法,其中花費時間比較長的且與線程安全無關的是37-39行代碼,會引起線程安全問題的是42-46。
1 public class ThreadSynch { 2 3 private int num; 4 5 public synchronized void longTimeTask(String userName){ 6 //定義各線程的進入時間 7 long thread0StartTime = 0L; 8 long thread1StartTime = 0L; 9 long thread2StartTime = 0L; 10 long thread3StartTime = 0L; 11 long thread4StartTime = 0L; 12 //定義各線程執行該方法所需的時間 13 long thread0LastTime; 14 long thread1LastTime; 15 long thread2LastTime; 16 long thread3LastTime; 17 long thread4LastTime; 18 //顯示各線程進入的時間 19 if(Thread.currentThread().getName().contains("-0")){ 20 thread0StartTime = System.currentTimeMillis(); 21 System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread0StartTime); 22 }else if(Thread.currentThread().getName().contains("-1")){ 23 thread1StartTime = System.currentTimeMillis(); 24 System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread1StartTime); 25 }else if(Thread.currentThread().getName().contains("-2")){ 26 thread2StartTime = System.currentTimeMillis(); 27 System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread2StartTime); 28 }else if(Thread.currentThread().getName().contains("-3")){ 29 thread3StartTime = System.currentTimeMillis(); 30 System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread3StartTime); 31 }else if(Thread.currentThread().getName().contains("-4")){ 32 thread4StartTime = System.currentTimeMillis(); 33 System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread4StartTime); 34 } 35 36 //花費時間較長,與線程安全無關的代碼 37 for(int i = 200000000; i > 0; i--) { 38 String nameID = Thread.currentThread().getName() + Thread.currentThread().getId(); 39 } 40 41 //與線程安全相關的代碼塊 42 if("zs".equals(userName)){ 43 num = 100; 44 }else if("ls".equals(userName)){ 45 num = 200; 46 } 47 48 //顯示各線程執行該方法的時間 49 if(Thread.currentThread().getName().contains("0")){ 50 thread0LastTime = System.currentTimeMillis() - thread0StartTime; 51 System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread0LastTime + "ms"); 52 }else if(Thread.currentThread().getName().contains("1")){ 53 thread1LastTime = System.currentTimeMillis() - thread1StartTime; 54 System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread1LastTime + "ms"); 55 }else if(Thread.currentThread().getName().contains("2")){ 56 thread2LastTime = System.currentTimeMillis() - thread2StartTime; 57 System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread2LastTime + "ms"); 58 }else if(Thread.currentThread().getName().contains("3")){ 59 thread3LastTime = System.currentTimeMillis() - thread3StartTime; 60 System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread3LastTime + "ms"); 61 }else if(Thread.currentThread().getName().contains("4")){ 62 thread4LastTime = System.currentTimeMillis() - thread4StartTime; 63 System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread4LastTime + "ms"); 64 } 65 66 } 67 }
繼承Thread的Thread01類,其run方法調用上述對象的longTimeTask方法
public class Thread01 extends Thread{ private ThreadSynch threadSynch; public Thread01(ThreadSynch threadSynch) { this.threadSynch = threadSynch; } @Override public void run() { threadSynch.longTimeTask("ls"); } }
測試,構建同一對象的多個線程
public class Test { public static void main(String[] args) { ThreadSynch threadSynch = new ThreadSynch(); //五個線程使用同一個對象構建 Thread thread01 = new Thread01(threadSynch); Thread thread02 = new Thread01(threadSynch); Thread thread03 = new Thread01(threadSynch); Thread thread04 = new Thread01(threadSynch); Thread thread05 = new Thread01(threadSynch); //五個線程同時調用該對象中的方法 thread01.start(); thread02.start(); thread03.start(); thread04.start(); thread05.start(); } }
結果:
Thread-0進入時間為====1553150692703 Thread-0執行時間為===8437ms Thread-3進入時間為====1553150701140 Thread-3執行時間為===7014ms Thread-1進入時間為====1553150708154 Thread-1執行時間為===7002ms Thread-4進入時間為====1553150715157 Thread-4執行時間為===7121ms Thread-2進入時間為====1553150722278 Thread-2執行時間為===7147ms
說明:因為synchronized修飾的是整個方法,所以線程Thread-0訪問longTimeTask方法的時候,其余四個線程都處於阻塞狀態,待其執行結束釋放鎖的時候,線程Thread-3開始執行,其余三個線程還是處於阻塞狀態,所以,這五個線程執行完畢所需的時間是各自執行時間的相加,8.4 + 7.0 + 7.0 + 7.1 + 7.1 = 36.6s。
2、synchronized修飾代碼塊(同步代碼塊)
synchronized由同步方法改為同步方法中引起線程安全問題的代碼塊,其余都不變
public class ThreadSynch { private int num; public void longTimeTask(String userName){ long thread0StartTime = 0L; long thread1StartTime = 0L; long thread2StartTime = 0L; long thread3StartTime = 0L; long thread4StartTime = 0L; long thread0LastTime; long thread1LastTime; long thread2LastTime; long thread3LastTime; long thread4LastTime; if(Thread.currentThread().getName().contains("-0")){ thread0StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread0StartTime); }else if(Thread.currentThread().getName().contains("-1")){ thread1StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread1StartTime); }else if(Thread.currentThread().getName().contains("-2")){ thread2StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread2StartTime); }else if(Thread.currentThread().getName().contains("-3")){ thread3StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread3StartTime); }else if(Thread.currentThread().getName().contains("-4")){ thread4StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread4StartTime); } //花費時間較長,與線程安全無關的代碼 for(int i = 200000000; i > 0; i--) { String nameID = Thread.currentThread().getName() + Thread.currentThread().getId(); } //與線程安全相關的代碼塊用synchronized修飾 synchronized(this){ if("zs".equals(userName)){ num = 100; }else if("ls".equals(userName)){ num = 200; } } if(Thread.currentThread().getName().contains("0")){ thread0LastTime = System.currentTimeMillis() - thread0StartTime; System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread0LastTime + "ms"); }else if(Thread.currentThread().getName().contains("1")){ thread1LastTime = System.currentTimeMillis() - thread1StartTime; System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread1LastTime + "ms"); }else if(Thread.currentThread().getName().contains("2")){ thread2LastTime = System.currentTimeMillis() - thread2StartTime; System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread2LastTime + "ms"); }else if(Thread.currentThread().getName().contains("3")){ thread3LastTime = System.currentTimeMillis() - thread3StartTime; System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread3LastTime + "ms"); }else if(Thread.currentThread().getName().contains("4")){ thread4LastTime = System.currentTimeMillis() - thread4StartTime; System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread4LastTime + "ms"); } } }
同樣的五個線程訪問,看一下結果:
Thread-0進入時間為====1553151204348 Thread-3進入時間為====1553151204348 Thread-1進入時間為====1553151204348 Thread-2進入時間為====1553151204348 Thread-4進入時間為====1553151204380 Thread-3執行時間為===19330ms Thread-2執行時間為===19383ms Thread-1執行時間為===19854ms Thread-4執行時間為===20498ms Thread-0執行時間為===20782ms
說明:因為synchronized修飾的是方法中會引起線程安全問題的代碼塊,所以僅僅是這一部分代碼無法並發執行。可以看到Thread-0,Thread-1,Thread-2,Thread-3,Thread-4幾乎同時進入longTimeTask方法,並發執行for循環中花費時間較長的代碼,由結果看,Thread-3最先執行完這部分代碼,開始執行synchronized修飾的代碼塊,其余四個線程隨后進入阻塞狀態。因為同步代碼塊中執行時間較短,Thread-3執行完后,Thread-2開始執行,最后是Thread-0執行,至此,五個線程執行完畢,所花費的時間就是Thread-0花費的時間,即20.8s。
可以看到,在longTimeTask方法中,synchronized由修飾方法改為修飾代碼塊,多線程執行所花費的時間由36.6s變成20.8s,執行時間明顯減少,效率提升。
二、任意對象作為對象監視器
2.1 上述同步代碼塊使用的是synchronized(this)格式,其實Java還支持對“任意對象”作為對象監視器來實現同步的功能。這種任意對象大多是該方法所屬類中的實例變量或該方法的參數,不然拋開這個類去使用別的對象作為對象監視器,意義不大。使用的格式是synchronized(非this的任意對象)。
舉例:以ThreadSynch類中的變量student作為對象監視器去同步代碼塊
public class ThreadSynch { private Student student = new Student(); private String schoolName; public void setNameAndPassWord(String name,String age){ synchronized(student){ System.out.println(Thread.currentThread().getName() + "===" + "進入同步代碼塊"); try { Thread.sleep(3000); this.student.setName(name); this.student.setAge(age); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "===" + "離開同步代碼塊"); } } }
Thread01的run方法調用setNameAndPassWord方法
public class Thread01 extends Thread{ private ThreadSynch threadSynch; public Thread01(ThreadSynch threadSynch) { this.threadSynch = threadSynch; } @Override public void run() { threadSynch.setNameAndPassWord("ls","11"); } }
測試:
public class Test { public static void main(String[] args) { ThreadSynch threadSynch = new ThreadSynch(); //三個線程使用同一個對象構建 Thread thread01 = new Thread01(threadSynch); Thread thread02 = new Thread01(threadSynch); Thread thread03 = new Thread01(threadSynch); //三個線程同時調用該對象中的方法 thread01.start(); thread02.start(); thread03.start(); } }
結果:
Thread-1===進入同步代碼塊 Thread-1===離開同步代碼塊 Thread-2===進入同步代碼塊 Thread-2===離開同步代碼塊 Thread-0===進入同步代碼塊 Thread-0===離開同步代碼塊
說明:Thread-0,Thread-1,Thread-2執行到同步代碼塊synchronized(student)時,都會去獲取與student對象關聯的monitor,判斷該monitor是否被別的線程所有,因為三個線程中的student都是同一個對象,所以一個線程執行的時候,與student關聯的那個monitor會被當前線程所有,別的線程都會處於阻塞狀態。
稍微改一下ThreadSynch類中setNameAndPassWord的方法,添加7-9行的代碼
1 public class ThreadSynch { 2 3 private Student student = new Student(); 4 private String schoolName; 5 6 public void setNameAndPassWord(String name,String age){ 7 if(Thread.currentThread().getName().contains("1")){ 8 student = new Student(); 9 } 10 synchronized(student){ 11 System.out.println(Thread.currentThread().getName() + "===" + "進入同步代碼塊"); 12 try { 13 Thread.sleep(3000); 14 this.student.setName(name); 15 this.student.setAge(age); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 System.out.println(Thread.currentThread().getName() + "===" + "離開同步代碼塊"); 20 } 21 } 22 }
其余都不變,看一下結果:
Thread-0===進入同步代碼塊 Thread-1===進入同步代碼塊 Thread-0===離開同步代碼塊 Thread-1===離開同步代碼塊 Thread-2===進入同步代碼塊 Thread-2===離開同步代碼塊
說明:可以看到,Thread-0和Thread-1同時進入同步代碼塊。分析一下原因,Thread-0執行到synchronized(student)時,會去獲取與該student對象關聯的monitor的所有權,該monitor沒有被別的線程占有,Thread-0進入同步代碼塊中。Thread-1執行setNameAndPassWord方法的時候,新添加的7-9行的代碼將student變量指向了一個新的student對象,此時的student對象和Thread-0時的student對象已經不是同一個了,對應的monitor也不是Thread-0時的那個monitor,所以Thread-1在Thread-0還未離開同步代碼塊的時候,也可以進入到同步代碼塊中執行。但Thread-2執行同步代碼塊時的student還是Thread-1時的那個student,所以Thread-2只能等到Thread-1執行結束,才能進入同步代碼塊中。
所以,多個線程訪問同步代碼塊時,只要synchronized(this對象/非this對象)中的對象是同一個對象,那么同一時間只能有一個線程可以執行同步代碼塊中的內容。這里注意一下當任意對象是string類型時,使用不當可能會有一些麻煩。具體就是以下兩個例子:
public class Test { public static void main(String[] args) { String str1 = "111"; String str2 = "111"; System.out.println(str1 == str2); String str3 = new String("222"); String str4 = new String("222"); System.out.println(str3 == str4); } }
結果:
true false
多線程並發執行時,當synchronized(str1)由str1變成str2時,其余線程是否還會處於阻塞狀態(會)。
多線程並發執行時,當synchronized(str3)由str3變成str4時,其余線程是否還會處於阻塞狀態(不會)。
具體的string常量與new String對象的區別,參見這篇文章從為什么String=String談到StringBuilder和StringBuffer。
參考資料:
