Synchronized及其實現原理
並發編程中synchronized一直是元老級角色,我們稱之為重量級鎖。主要用在三個地方:
1、修飾普通方法,鎖是當前實例對象。
2、修飾類方法,鎖是當前類的Class對象。
3、修飾代碼塊,鎖是synchronized括號裏面的對象。
一、synchronized實現原理
當一個線程試圖訪問同步代碼塊時,必須得到鎖。在退出或拋出異常時必須釋放鎖,JVM是基於進入和退出Monitor來實現方法同步和代碼塊同步。
我們來看下synchronized的字節碼:
public class SynchronizedTest { public void addNum1(String userName) { } public void addNum2(String userName) { synchronized(this) { } } public synchronized void addNum3(String userName) { } }
在字節碼裏可以看到,用synchronizde修飾的同步代碼塊多了兩個指令:monitorenter、monitorexit;
代碼塊同步是使用monitorenter、monitorexit指令實現的,而方法同步是使用另外一種方式實現的,但是方法同步也可以使用這兩個指令來實現。
monitorenter指令是編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法的結束和異常位置。任何一個對象都有一個monitor與之關聯。線程執行到monitorenter指令處時,會嘗試獲取對象所對應的monitor所有權,即嘗試獲得對象的鎖。
二、修飾普通方法 鎖是當前實例對象
我們先來看下將實例對象作為鎖的概念:
public class AddNumTest { private int num = 0; public synchronized void addNum(String str) { try { if ("a".equals(str)) { num = 10; System.out.println("add a"); Thread.sleep(2000); } else { num = 20; System.out.println("add b"); } System.out.println(str + " num = " + num); } catch (InterruptedException e) { e.printStackTrace(); } } }
public class AddNumThreadOne implements Runnable { private AddNumTest at; public AddNumThreadOne(AddNumTest at) { this.at = at; } @Override public void run() { at.addNum("a"); } }
public class AddNumThreadTwo implements Runnable { private AddNumTest at; public AddNumThreadTwo(AddNumTest at) { this.at = at; } @Override public void run() { at.addNum("b"); } }
public class AddNum { public static void main(String[] args) { //註意,這裏傳入同一個實例對象 AddNumTest at = new AddNumTest(); //AddNumTest bt = new AddNumTest(); Thread t1 = new Thread(new AddNumThreadOne(at)); Thread t2 = new Thread(new AddNumThreadTwo(at)); t1.start(); t2.start(); } }
執行結果:
add a a num = 10add b b num = 20
前面解釋過關鍵字synchronized的實現原理是使用對象的monitor來實現的,取的鎖都是對象鎖,而不是把一段代碼或者函數作為鎖。在並發情況下,如果並發情況下多線程競爭的是同一個對象,那麽先來的獲取該對象鎖,後面的線程只能排隊,等前面的線程執行完畢釋放鎖。
上面介紹的是同一個對象鎖,我們來觀察下獲取不同的對象鎖會是什麽情況:
public static void main(String[] args) { //註意,這裏傳入的不同的實例對象 AddNumTest at = new AddNumTest(); AddNumTest bt = new AddNumTest(); Thread t1 = new Thread(new AddNumThreadOne(at)); Thread t2 = new Thread(new AddNumThreadTwo(bt)); t1.start(); t2.start(); }
執行結果:
add a add b b num = 20a num = 10
這裏線程1、2搶占的是不同的鎖,盡管線程1先到達同步代碼塊的位置,但是由於monitor不一樣,所以不能阻塞線程2的執行。
三、synchronized鎖重入
鎖重入:當一個線程得到一個對象鎖後,再次請求此對象鎖時可以再次得到該對象的鎖。但是這裏有維護一個計數器,同一個線程每次得到對象鎖計數器都會加1,釋放的時候減1,直到計數器的數值為0的時候,才能被其他線程所搶占
public class AgainLock { public synchronized void print1() { System.out.println("do work print1"); print2(); } public synchronized void print2() { System.out.println("do work print2"); print3(); } public synchronized void print3() { System.out.println("do work print3"); } }
public class AgainLockTest { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { AgainLock al = new AgainLock(); al.print1(); } }); t.start(); } }
執行結果:
do work print1do work print2do work print3
這裏面三個同步方法,使用的鎖都是該實例對象的同步鎖,同一個線程在執行的時候,每次都是在鎖沒有釋放的時候,就要重新再去獲取同一把對象鎖。從運行結果可以看出,關鍵字synchronized支持同一線程鎖重入。
四、synchronized同步代碼塊
用synchronized同步方法的粒度過大,有時候一個方法裏面的業務邏輯很多,但是我們想對同步的部分進行單獨定制,這時候就可以使用synchronized來同步代碼塊。
public class SynchronizedTest1 { public void doWorkTask() { for(int i=0;i<100;i++) { System.out.println("nosynchronized thread name =" + Thread.currentThread().getName() + ";i=" + i); } synchronized(this) { for(int i=0;i<100;i++) { System.out.println("thread name =" + Thread.currentThread().getName() + ";i=" + i); } } } }
如果在並發情況下,調用這個類的同一個實例,線程A和B可以同時執行使用synchronized同步之前的代碼邏輯,但是使用關鍵字同步的部分是互斥的,先到達的線程占有對象鎖,後面的線程會被阻塞,直到對象鎖被前面的線程釋放。
Synchronized及其實現原理