Android進(jìn)階之徹底理解Synchronized關(guān)鍵字
本文轉(zhuǎn)載自微信公眾號(hào)「Android開發(fā)編程」,作者Android開發(fā)編程。轉(zhuǎn)載本文請(qǐng)聯(lián)系A(chǔ)ndroid開發(fā)編程公眾號(hào)。
一、Synchronized詳解
synchronized是Java中的一個(gè)關(guān)鍵字,在多線程共同操作共享資源的情況下,可以保證在同一時(shí)刻只有一個(gè)線程可以對(duì)共享資源進(jìn)行操作,從而實(shí)現(xiàn)共享資源的線程安全。
二、Synchronized的特性
- 原子性。synchronized可以確保多線程下對(duì)共享資源的互斥訪問,被synchronized作用的代碼可以實(shí)現(xiàn)原子性。
- 可見性。synchronized保證對(duì)共享資源的修改能夠及時(shí)被看見。在Java內(nèi)存模型中,對(duì)一個(gè)共享變量操作后進(jìn)行釋放鎖即進(jìn)行unlock操作前,必須將修改同步到主內(nèi)存中。如果對(duì)一個(gè)共享資源進(jìn)行加鎖即lock操作之前,必須將工作內(nèi)存中共享變量的值清空(因?yàn)槊恳粋€(gè)線程獲取的共享變量都是主存中共享變量的一個(gè)副本,如果不進(jìn)行清空,就會(huì)發(fā)生數(shù)據(jù)不一致,即當(dāng)前線程中的共享變量與主存中的共享變量不一致),在使用此共享變量時(shí),就需要從主存中重新加載此共享變量以獲得該共享變量最新的值。
- 有序性。synchronized可以有效解決重排序問題,即一個(gè)unlock解鎖操作必定先行發(fā)生于后面線程對(duì)同一個(gè)鎖的lock操作,這樣就會(huì)保證主內(nèi)存值的共享變量永遠(yuǎn)是最新的。
三、Synchronized的使用
在應(yīng)用Sychronized關(guān)鍵字時(shí)需要把握如下注意點(diǎn):
一把鎖只能同時(shí)被一個(gè)線程獲取,沒有獲得鎖的線程只能等待;
每個(gè)實(shí)例都對(duì)應(yīng)有自己的一把鎖(this),不同實(shí)例之間互不影響;例外:鎖對(duì)象是*.class以及synchronized修飾的是static方法的時(shí)候,所有對(duì)象公用同一把鎖;
synchronized修飾的方法,無論方法正常執(zhí)行完畢還是拋出異常,都會(huì)釋放鎖。
對(duì)象鎖
包括方法鎖(默認(rèn)鎖對(duì)象為this,當(dāng)前實(shí)例對(duì)象)和同步代碼塊鎖(自己指定鎖對(duì)象鎖)
代碼塊形式:手動(dòng)指定鎖定對(duì)象,也可是是this,也可以是自定義的鎖
- public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); // 創(chuàng)建2把鎖 Object block1 = new Object(); Object block2 = new Object(); @Override public void run() { // 這個(gè)代碼塊使用的是第一把鎖,當(dāng)他釋放后,后面的代碼塊由于使用的是第二把鎖,因此可以馬上執(zhí)行 synchronized (block1) { System.out.println("block1鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block1鎖,"+Thread.currentThread().getName() + "結(jié)束"); } synchronized (block2) { System.out.println("block2鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block2鎖,"+Thread.currentThread().getName() + "結(jié)束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } } 復(fù)制代碼
輸出結(jié)果:
- block1鎖,我是線程Thread-0 block1鎖,Thread-0結(jié)束 block2鎖,我是線程Thread-0 // 可以看到當(dāng)?shù)谝粋€(gè)線程在執(zhí)行完第一段同步代碼塊之后,第二個(gè)同步代碼塊可以馬上得到執(zhí)行,因?yàn)樗麄兪褂玫逆i不是同一把 block1鎖,我是線程Thread-1 block2鎖,Thread-0結(jié)束 block1鎖,Thread-1結(jié)束 block2鎖,我是線程Thread-1 block2鎖,Thread-1結(jié)束
方法鎖形式:synchronized修飾普通方法,鎖對(duì)象默認(rèn)為this
- public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { method(); } public synchronized void method() { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結(jié)束"); } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }
類鎖
包括方法鎖(默認(rèn)鎖對(duì)象為this,當(dāng)前實(shí)例對(duì)象)和同步代碼塊鎖(自己指定鎖對(duì)象)
synchronize修飾靜態(tài)方法(類的class對(duì)象)
- public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在靜態(tài)方法上,默認(rèn)的鎖就是當(dāng)前所在的Class類,所以無論是哪個(gè)線程訪問它,需要的鎖都只有一把 public static synchronized void method() { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結(jié)束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }
輸出結(jié)果:
我是線程Thread-0 Thread-0結(jié)束 我是線程Thread-1 Thread-1結(jié)束 復(fù)制代碼
synchronized修改實(shí)例方法
- /** * synchronized修飾實(shí)例方法,當(dāng)線程拿到鎖,其他線程無法拿到該對(duì)象的鎖,那么其他線程就無法訪問該對(duì)象的其他同步方法 * 但是可以訪問該對(duì)象的其他非synchronized方法 * 鎖住的是類的實(shí)例對(duì)象 */ public class synchronizedDemo1 implements Runnable { //模擬一個(gè)共享數(shù)據(jù) private static int total=0; //同步方法,每個(gè)線程獲取到鎖之后,執(zhí)行5次累加操作 public synchronized void increase(){ for (int i = 1; i < 6; i++) { System.out.println(Thread.currentThread().getName()+"執(zhí)行累加操作..."+"第"+i+"次累加"); try { total=total+1; Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } //實(shí)例對(duì)象的另一個(gè)同步方法 public synchronized void declare(){ System.out.println(Thread.currentThread().getName()+"執(zhí)行total-1"); total--; System.out.println(Thread.currentThread().getName()+"執(zhí)行total-1完成"); } //普通實(shí)例方法 public void simpleMethod(){ System.out.println(Thread.currentThread().getName()+ " ----實(shí)例對(duì)象的普通方法---"); } @Override public void run() { //線程執(zhí)行體 System.out.println(Thread.currentThread().getName()+"準(zhǔn)備執(zhí)行累加,還沒獲取到鎖"); //執(zhí)行普通方法 simpleMethod(); //調(diào)用同步方法執(zhí)行累加操作 increase(); //執(zhí)行完increase同步方法后,會(huì)釋放掉鎖,然后線程1和線程2會(huì)再一次進(jìn)行鎖的競(jìng)爭(zhēng),誰先競(jìng)爭(zhēng)得到鎖,誰就先執(zhí)行declare同步方法 System.out.println(Thread.currentThread().getName()+"完成累加操作"); //調(diào)用實(shí)例對(duì)象的另一個(gè)同步方法 System.out.println(Thread.currentThread().getName()+"準(zhǔn)備執(zhí)行total-1"); declare(); } public static void main(String[] args) throws InterruptedException { synchronizedDemo1 syn = new synchronizedDemo1(); Thread thread1 = new Thread(syn,"線程1"); Thread thread2 = new Thread(syn,"線程2"); thread1.start(); thread2.start(); } }
輸出結(jié)果:
線程1準(zhǔn)備執(zhí)行累加,還沒獲取到鎖 線程2準(zhǔn)備執(zhí)行累加,還沒獲取到鎖 線程2 ----實(shí)例對(duì)象的普通方法--- 線程2執(zhí)行累加操作...第1次累加 //線程2通過與線程1的競(jìng)爭(zhēng)率先拿到了鎖,進(jìn)入increase同步方法 線程2執(zhí)行累加操作...第2次累加 線程1 ----實(shí)例對(duì)象的普通方法--- //從這里可看出,在線程2訪問同步方法時(shí),線程1是可以訪問非同步方法的,但是不可以訪問另外一個(gè)同步方法 線程2執(zhí)行累加操作...第3次累加 線程2執(zhí)行累加操作...第4次累加 線程2執(zhí)行累加操作...第5次累加 線程2完成累加操作 //線程2執(zhí)行累加后會(huì)釋放掉鎖 線程2準(zhǔn)備執(zhí)行total-1 線程1執(zhí)行累加操作...第1次累加 //然后線程1拿到鎖后進(jìn)入increase同步方法執(zhí)行累加 線程1執(zhí)行累加操作...第2次累加 線程1執(zhí)行累加操作...第3次累加 線程1執(zhí)行累加操作...第4次累加 線程1執(zhí)行累加操作...第5次累加 線程1完成累加操作 //線程1完成累加操作也會(huì)釋放掉鎖,然后線程1和線程2會(huì)再進(jìn)行一次鎖競(jìng)爭(zhēng) 線程1準(zhǔn)備執(zhí)行total-1 線程2執(zhí)行total-1 //線程2通過競(jìng)爭(zhēng)率先拿到鎖進(jìn)入declear方法執(zhí)行total-1操作 線程2執(zhí)行total-1完成 線程1執(zhí)行total-1 線程1執(zhí)行total-1完成 復(fù)制代碼
四、Synchronized實(shí)現(xiàn)原理
加鎖和釋放鎖
synchronized同步是通過monitorenter和monitorexit等指令實(shí)現(xiàn)的,會(huì)讓對(duì)象在執(zhí)行,使其鎖計(jì)數(shù)器加1或者減1。
monitorenter指令:每一個(gè)對(duì)象在同一時(shí)間只與一個(gè)monitor(鎖)相關(guān)聯(lián),而一個(gè)monitor在同一時(shí)間只能被一個(gè)線程獲得,一個(gè)對(duì)象在嘗試獲得與這個(gè)對(duì)象相關(guān)聯(lián)的Monitor鎖的所有權(quán)的時(shí)候,會(huì)發(fā)生如下3種情況之一:
- monitor計(jì)數(shù)器為0,意味著目前還沒有被獲得,那這個(gè)線程就會(huì)立刻獲得然后把鎖計(jì)數(shù)器+1,一旦+1,別的線程再想獲取,就需要等待
- 如果這個(gè)monitor已經(jīng)拿到了這個(gè)鎖的所有權(quán),又重入了這把鎖,那鎖計(jì)數(shù)器就會(huì)累加,變成2,并且隨著重入的次數(shù),會(huì)一直累加
- 若其他線程已經(jīng)持有了對(duì)象監(jiān)視器,則當(dāng)前線程進(jìn)入阻塞狀態(tài),直到對(duì)象監(jiān)視器的進(jìn)入數(shù)為0,重新嘗試獲取monitor的所有權(quán)。
monitorexit指令:釋放對(duì)于monitor的所有權(quán),釋放過程很簡(jiǎn)單,就是講monitor的計(jì)數(shù)器減1,如果減完以后,計(jì)數(shù)器不是0,則代表剛才是重入進(jìn)來的,當(dāng)前線程還繼續(xù)持有這把鎖的所有權(quán),如果計(jì)數(shù)器變成0,則代表當(dāng)前線程不再擁有該monitor的所有權(quán),即釋放鎖。
對(duì)象、對(duì)象監(jiān)視器、同步隊(duì)列以及執(zhí)行線程狀態(tài)之間的關(guān)系:
該圖可以看出,任意線程對(duì)Object的訪問,首先要獲得Object的監(jiān)視器,如果獲取失敗,該線程就進(jìn)入同步狀態(tài),線程狀態(tài)變?yōu)锽LOCKED,當(dāng)Object的監(jiān)視器占有者釋放后,在同步隊(duì)列中得線程就會(huì)有機(jī)會(huì)重新獲取該監(jiān)視器。
可重入原理:加鎖次數(shù)計(jì)數(shù)器
從上圖中就可以看出來,執(zhí)行靜態(tài)同步方法的時(shí)候就只有一條monitorexit指令,并沒有monitorenter獲取鎖的指令。這就是鎖的重入性,即在同一鎖程中,線程不需要再次獲取同一把鎖。
Synchronized先天具有重入性。每個(gè)對(duì)象擁有一個(gè)計(jì)數(shù)器,當(dāng)線程獲取該對(duì)象鎖后,計(jì)數(shù)器就會(huì)加一,釋放鎖后就會(huì)將計(jì)數(shù)器減一。
保證可見性的原理:內(nèi)存模型和happens-before規(guī)則
Synchronized的happens-before規(guī)則,即監(jiān)視器鎖規(guī)則:對(duì)同一個(gè)監(jiān)視器的解鎖,happens-before于對(duì)該監(jiān)視器的加鎖。
public class MonitorDemo { private int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 } // 6 } 復(fù)制代碼
happens-before關(guān)系如圖所示:
在圖中每一個(gè)箭頭連接的兩個(gè)節(jié)點(diǎn)就代表之間的happens-before關(guān)系,黑色的是通過程序順序規(guī)則推導(dǎo)出來,紅色的為監(jiān)視器鎖規(guī)則推導(dǎo)而出:線程A釋放鎖happens-before線程B加鎖,藍(lán)色的則是通過程序順序規(guī)則和監(jiān)視器鎖規(guī)則推測(cè)出來happens-befor關(guān)系,通過傳遞性規(guī)則進(jìn)一步推導(dǎo)的happens-before關(guān)系。
總結(jié)
- synchronized同步語句塊的實(shí)現(xiàn)使?的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代碼塊的開始位置, monitorexit指令則指明同步代碼塊的結(jié)束位置。
- synchronized修飾的?法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實(shí)是ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該?法是?個(gè)同步?法。
不過兩者的本質(zhì)都是對(duì)對(duì)象監(jiān)視器 monitor 的獲取。
使用Synchronized有哪些要注意的?
- 鎖對(duì)象不能為空,因?yàn)殒i的信息都保存在對(duì)象頭里;
- 作用域不宜過大,影響程序執(zhí)行的速度,控制范圍過大,編寫代碼也容易出錯(cuò);
- 避免死鎖;
- 在能選擇的情況下,既不要用Lock也不要用synchronized關(guān)鍵字,用java.util.concurrent包中的各種各樣的類,如果不用該包下的類,在滿足業(yè)務(wù)的情況下,可以使用synchronized關(guān)鍵字,因?yàn)榇a量少,避免出錯(cuò)。