synchronized關鍵字使用詳解
簡述
計算機單執行緒在執行任務時,是嚴格按照程式的程式碼邏輯,按照順序執行的。因此單位時間內能執行的任務數量有限。為了能在相同的時間內能執行更多的任務,就必須採用多執行緒的方式來執行(注意:多執行緒模式無法減少單次任務的執行時間 )。但是引入了多執行緒之後,又帶來了執行緒安全的問題。而為了解決執行緒安全的問題,又引入了鎖的概念。java中常用的鎖有synchronized 和lock 兩種,本文我們來分析synchronized 的具體用法和使用注意事項。
基本使用
同步程式碼塊
/** * 同步程式碼塊 * @throws Exception */ public void synchronizedCode() { try { synchronized (this) { System.out.println(getCurrentTime() + ":I am synchronized Code"); Thread.sleep(5000);//延時5秒,方便後面測試 } } catch (Exception e) { e.printStackTrace(); } }
作用程式碼塊時,synchronized方法中的this,是指呼叫該方法的物件。需要主要的是,synchronized作用程式碼塊時,只會鎖住這一小塊程式碼。程式碼塊的上下部分的其他程式碼在所有的執行緒仍然是能同時訪問的。同時需要注意的是每個物件有用不同的鎖。即不會阻塞不同物件的呼叫。
同步方法
/** * 同步方法 */ public synchronized void synchronizedMethod() { try { System.out.println(getCurrentTime() + ":I am synchronized method"); Thread.sleep(5000);//延時5秒,方便後面測試 } catch (Exception e) { e.printStackTrace(); } }
synchronized作用在方法上,其實是缺省了this關鍵字,實際上是synchronized(this)。this是指呼叫該方法的物件。此鎖也不會阻塞不同物件之間的呼叫。
同步靜態方法
/** * 同步靜態方法 */ public synchronized static void synchronizedStaticMethod() { try { System.out.println(getCurrentTime() + ":I am synchronized static method"); Thread.sleep(5000);//延時5秒,方便後面測試 } catch (Exception e) { e.printStackTrace(); } }
使用方式和作用普通方式相同,唯一需要注意的地方是此鎖所有物件共用,即不同物件之間會阻塞呼叫。
測試準備
簡單說明一下:有一個執行緒池,在執行多工時使用。每個同步方法或者程式碼塊中都有一個休眠5秒的動作,利用列印時間加休眠來看執行緒之間是否有阻塞效果。然後有一個1秒列印一次時間的方法。
public class Synchronized { //列印時間時格式化 public static final String timeFormat = "HH:mm:ss"; //執行多工的執行緒池 public static final ExecutorService executor = Executors.newFixedThreadPool(4); /** * 同步程式碼塊 * @throws Exception */ public void synchronizedCode() { try { synchronized (this) { System.out.println(getCurrentTime() + ":I am synchronized Code"); Thread.sleep(5000);//延時5秒,方便後面測試 } } catch (Exception e) { e.printStackTrace(); } } /** * 同步方法 */ public synchronized void synchronizedMethod() { try { System.out.println(getCurrentTime() + ":I am synchronized method"); Thread.sleep(5000);//延時5秒,方便後面測試 } catch (Exception e) { e.printStackTrace(); } } /** * 同步靜態方法 */ public synchronized static void synchronizedStaticMethod() { try { System.out.println(getCurrentTime() + ":I am synchronized static method"); Thread.sleep(5000);//延時5秒,方便後面測試 } catch (Exception e) { e.printStackTrace(); } } /** * 迴圈列印時間 */ public static void printNumber() { executor.execute(new Runnable() { @Override public void run() { while (true) { try { printOnceASecond(); } catch (Exception e) { e.printStackTrace(); } } } }); } /** * 一秒列印一次時間 * * @throws Exception */ public static void printOnceASecond() throws Exception { System.out.println(getCurrentTime()); Thread.sleep(1000); } /** * 獲取當前時間 * * @return */ public static String getCurrentTime() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(timeFormat)); } }
OK,接下來我們就來測試下鎖的互斥性以及使用注意事項(都是多執行緒 的情況下)。
開始測試
同一個物件同步程式碼塊
public static void main(String[] args) throws Exception { printNumber();//控制檯迴圈列印時間 Synchronized es = new Synchronized(); executor.execute(() -> es.synchronizedCode()); executor.execute(() -> es.synchronizedCode()); }
execute
20:34:41:I am synchronized Code 20:34:41 20:34:42 20:34:43 20:34:44 20:34:45 20:34:46:I am synchronized Code
同步程式碼塊中休眠5秒,導致另外一個執行緒阻塞5秒後再執行。說明代同步碼塊會阻塞同一個物件的不同執行緒之間的呼叫(同步方法和同步靜態方法也會阻塞同一個物件的不同執行緒之間的呼叫,此處省略測試程式碼)
不同物件同步程式碼塊
public static void main(String[] args) throws Exception { printNumber();//控制檯迴圈列印時間 Synchronized es = new Synchronized(); Synchronized es1 = new Synchronized(); executor.execute(() -> es.synchronizedCode()); executor.execute(() -> es1.synchronizedCode()); }
execute
20:44:34:I am synchronized Code 20:44:34:I am synchronized Code
由結果可以看出,不同物件之間程式碼塊鎖互不影響(多執行緒也一樣)。原因是因為程式碼塊中synchronized (this)
鎖的是當前呼叫物件,不同物件之間不是同一把鎖,因此互不影響(同步方法原理也是如此,省略測試程式碼)。
同一物件同步程式碼塊和方法
public static void main(String[] args) throws Exception { printNumber();//控制檯迴圈列印時間 Synchronized es = new Synchronized(); executor.execute(() -> es.synchronizedCode()); executor.execute(() -> es.synchronizedMethod()); }
execute
20:51:27:I am synchronized method 20:51:27 20:51:28 20:51:29 20:51:30 20:51:31 20:51:32:I am synchronized Code
因為同步程式碼塊和同步方法,都是鎖當前呼叫物件,因此執行後列印上述結果應該在意料之中。基於這樣的特性,實際開發在使用spring的時候就需要注意了,我們的bean交給spring容器管理之後,預設都是單例的。那麼這個時候使用synchronized 關鍵字就需要注意了(推薦使用同步程式碼塊,同步的程式碼塊中傳入外部定義的一個變數)。
不同物件靜態同步方法
public static void main(String[] args) throws Exception { printNumber();//控制檯迴圈列印時間 Synchronized es = new Synchronized(); Synchronized es1 = new Synchronized(); executor.execute(() -> es.synchronizedStaticMethod()); executor.execute(() -> es1.synchronizedStaticMethod()); }
execute
21:05:39:I am synchronized static method 21:05:40 21:05:41 21:05:42 21:05:43 21:05:44:I am synchronized static method
由上述結果可以看出來,靜態同步方法會阻塞所有的物件。原因是所有的靜態同步方法都是佔用的同一把鎖。
相同物件同步方法和靜態同步方法
public static void main(String[] args) throws Exception { printNumber();//控制檯迴圈列印時間 Synchronized es = new Synchronized(); executor.execute(() -> es.synchronizedMethod()); executor.execute(() -> es.synchronizedStaticMethod()); }
execute
21:11:03:I am synchronized static method 21:11:03:I am synchronized method
由此結果可以看出,同步方法和靜態同步方法之間不會造成阻塞的現象。因為他們鎖的物件不一樣。同步方法佔用的鎖是呼叫物件,靜態同步方法鎖的是編譯後的class物件。
總結
不同物件之間,同步方法、同步程式碼塊不會互斥。靜態同步方法會互斥