為了方便記憶,將鎖做如下的分類
一、對象鎖
包括方法鎖(默認鎖對象為this,當前實例對象)和同步代碼塊鎖(自己指定鎖對象)
1.代碼塊形式:手動指定鎖定對象,也可是是this,也可以是自定義的鎖
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { // 同步代碼塊形式——鎖為this,兩個線程使用的鎖是一樣的,線程1必須要等到線程0釋放了該鎖后,才能執行 synchronized (this) { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }
輸出結果:
我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); // 創建2把鎖 Object block1 = new Object(); Object block2 = new Object(); @Override public void run() { // 這個代碼塊使用的是第一把鎖,當他釋放后,后面的代碼塊由於使用的是第二把鎖,因此可以馬上執行 synchronized (block1) { System.out.println("blocl1鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("blocl1鎖,"+Thread.currentThread().getName() + "結束"); } synchronized (block2) { System.out.println("blocl2鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("blocl2鎖,"+Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); }
輸出結果:
blocl1鎖,我是線程Thread-0
blocl1鎖,Thread-0結束
blocl2鎖,我是線程Thread-0 // 可以看到當第一個線程在執行完第一段同步代碼塊之后,第二個同步代碼塊可以馬上得到執行,因為他們使用的鎖不是同一把
blocl1鎖,我是線程Thread-1
blocl2鎖,Thread-0結束
blocl1鎖,Thread-1結束
blocl2鎖,我是線程Thread-1
blocl2鎖,Thread-1結束
2.方法鎖形式:synchronized修飾普通方法,鎖對象默認為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() + "結束"); } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }
輸出結果:
我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束
二、類鎖
指synchronize修飾靜態的方法或指定鎖對象為Class對象
1.synchronize修飾靜態方法
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在普通方法上,默認的鎖就是this,當前實例 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() + "結束"); } public static void main(String[] args) { // t1和t2對應的this是兩個不同的實例,所以代碼不會串行 Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }
輸出結果:
我是線程Thread-0
我是線程Thread-1
Thread-1結束
Thread-0結束
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在靜態方法上,默認的鎖就是當前所在的Class類,所以無論是哪個線程訪問它,需要的鎖都只有一把 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() + "結束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }
輸出結果:
我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束
2.synchronized指定鎖對象為Class對象
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { // 所有線程需要的鎖都是同一把 synchronized(SynchronizedObjectLock.class){ System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }
輸出結果:
我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束
三、思考
1.兩個線程同時訪問1個對象的同步方法
2.兩個線程同時訪問2個對象的同步方法
3.兩個線程訪問的是synchronized靜態方法
4.兩個線程同時訪問同步(被synchronized修飾)和非同步(未被snychronized修飾)方法
5.兩個線程同時訪問1個對象的不同的普通同步方法
6.兩個線程同時訪問一個靜態的synchronized方法和非靜態的synchronized方法
7.方法拋出異常后,會釋放鎖嗎?
核心思想:
1.一把鎖只能同時被一個線程獲取,沒有難道鎖的線程只能等待(對應上面的1,5)
2.每個實例都對應有自己的一把鎖(this),不同實例之間互不影響;例外:鎖對象是*.class以及synchronized修飾的是static方法的時候,所有對象公用同一把鎖(對應上面的2,3,4,6)
3.synchronized修飾的方法,無論方法正常執行完畢還是拋出異常,都會釋放鎖(對應上面的7)
四、synchronized的性質
1.可重入性
概念:指同一個線程外層函數獲取到鎖之后,內層函數可以直接使用該鎖
好處:避免死鎖,提升封裝性(如果不可重入,假設method1拿到鎖之后,在method1中又調用了method2,如果method2沒辦法使用method1拿到的鎖,那method2將一直等待,但是method1由於未執行完畢,又無法釋放鎖,就導致了死鎖,可重入正好避免這這種情況)
粒度:線程而非調用(用3中情況來說明與pthread的區別)
1)情況1:證明同一個方法是可重入的(遞歸)
public class SynchronizedDemo2 { int a = 0; public static void main(String[] args) { new SynchronizedDemo2().method1(); } public synchronized void method1() { System.out.println("a=" + a); if (a == 0) { a++; method1(); } } }
輸出結果:
a=0
a=1
2)情況2:證明可重入不要求是同一個方法
public class SynchronizedDemo2 { public static void main(String[] args) { new SynchronizedDemo2().method1(); } public synchronized void method1() { System.out.println("method1"); method2(); } public synchronized void method2() { System.out.println("method2"); } }
輸出結果:
method1
method2
3)情況3:證明可重入不要求是同一個類中
public class SynchronizedDemo2 { public synchronized void method1() { System.out.println("父類method1"); } } class SubClass extends SynchronizedDemo2 { public synchronized void method1() { System.out.println("子類method1"); super.method1(); } public static void main(String[] args) { new SubClass().method1(); } }
輸出結果:
子類method1
父類method1
2.不可中斷性
概念:如果這個鎖被B線程獲取,如果A線程想要獲取這把鎖,只能選擇等待或者阻塞,直到B線程釋放這把鎖,如果B線程一直不釋放這把鎖,那么A線程將一直等待。
相比之下,未來的Lock類,可以擁有中斷的能力(如果一個線程等待鎖的時間太長了,有權利中斷當前已經獲取鎖的線程的執行,也可以退出等待)
五、深入原理
1.加鎖和釋放鎖的原理:現象、時機(內置鎖this)、深入JVM看字節碼(反編譯看monitor指令)
Lock lock = new ReentrantLock(); public synchronized void method1() { System.out.println("synchronized method1"); } public void method2() { lock.lock(); try { System.out.println("lock method2"); } finally { lock.unlock(); }
}
method1與method2等價,synchronized相當於先獲取鎖,執行結束/拋出異常后,釋放鎖。
深入JVM看字節碼,創建如下的代碼:
public class SynchronizedDemo2 { Object object = new Object(); public void method1() { synchronized (object) { } } }
使用javac命令進行編譯生成.class文件 >javac SynchronizedDemo2.java 使用javap命令反編譯查看.class文件的信息 >javap -verbose SynchronizedDemo2.class 得到如下的信息:
關注紅色方框里的monitorenter和monitorexit即可。
Monitorenter和Monitorexit指令,會讓對象在執行,使其鎖計數器加1或者減1。每一個對象在同一時間只與一個monitor(鎖)相關聯,而一個monitor在同一時間只能被一個線程獲得,一個對象在嘗試獲得與這個對象相關聯的Monitor鎖的所有權的時候,monitorenter指令會發生如下3中情況之一:
1)monitor計數器為0,意味着目前還沒有被獲得,那這個線程就會立刻獲得然后把鎖計數器+1,一旦+1,別的線程再想獲取,就需要等待
2)如果這個monitor已經拿到了這個鎖的所有權,又重入了這把鎖,那鎖計數器就會累加,變成2,並且隨着重入的次數,會一直累加
3)這把鎖已經被別的線程獲取了,等待鎖釋放
monitorexit指令:釋放對於monitor的所有權,釋放過程很簡單,就是講monitor的計數器減1,如果減完以后,計數器不是0,則代表剛才是重入進來的,當前線程還繼續持有這把鎖的所有權,如果計數器變成0,則代表當前線程不再擁有該monitor的所有權,即釋放鎖。
2.可重入原理:加鎖次數計數器
jvm會負責跟蹤對象被加鎖的次數
線程第一次獲得所,計數器+1,當鎖重入的時候,計數器會遞增
當任務離開的時候(一個同步代碼塊的代碼執行結束),計數器會減1,當減為0的時候,鎖被完全釋放。
3.保證可見性的原理:內存模型
訪問鏈接 https://www.cnblogs.com/xyabk/p/10894384.html
六、synchronized的缺陷
效率低:鎖的釋放情況少,只有代碼執行完畢或者異常結束才會釋放鎖;試圖獲取鎖的時候不能設定超時,不能中斷一個正在使用鎖的線程,相對而言,Lock可以中斷和設置超時
不夠靈活:加鎖和釋放的時機單一,每個鎖僅有一個單一的條件(某個對象),相對而言,讀寫鎖更加靈活
無法知道是否成功獲得鎖,相對而言,Lock可以拿到狀態,如果成功獲取鎖,....,如果獲取失敗,.....
七、Lock對synchronized的彌補
Lock類這里不做過多解釋,主要看上面紅色方框里面的4個方法
lock():加鎖
unlock():解鎖
tryLock():嘗試獲取鎖,返回一個boolean值
tryLock(long,TimeUtil):嘗試獲取鎖,可以設置超時
八、注意
1.鎖對象不能為空,因為鎖的信息都保存在對象頭里
2.作用域不宜過大,影響程序執行的速度,控制范圍過大,編寫代碼也容易出錯
3.避免死鎖
4.在能選擇的情況下,既不要用Lock也不要用synchronized關鍵字,用java.util.concurrent包中的各種各樣的類,如果不用該包下的類,在滿足業務的情況下,可以使用synchronized關鍵,因為代碼量少,避免出錯
九、思考
1.多個線程等待同一個snchronized鎖的時候,JVM如何選擇下一個獲取鎖的線程?
2.Synchronized使得同時只有一個線程可以執行,性能比較差,有什么提升的方法?
3.我想更加靈活地控制鎖的釋放和獲取(現在釋放鎖和獲取鎖的時機都被規定死了),怎么辦?
4.什么是鎖的升級和降級?什么事JVM里的偏斜鎖、輕量級鎖、重量級鎖?