Java内置锁——synchronized
一、給對象加把鎖
synchronized關鍵字是Java唯一內置的互斥鎖,通過關鍵字 synchronized 可以保證同一時刻只有一個線程獲得某個同步代碼塊的執行權,但不會導致其他線程執行非同步方法時阻塞。
當獲得鎖的線程執行完同步代碼塊后,線程會將鎖釋放,其他由于鎖占用導致阻塞的線程可以通過非公平的方式(非公平指的是獲得鎖的操作不是按照請求鎖的順序,即沒有先來后到之分)獲得鎖,并進入同步代碼塊執行代碼。
雖然我們通常要在方法上或是為某個代碼塊標記 synchronized,但實際上,JVM實現鎖的邏輯是通過給對象加鎖。
如果將一個成員方法標記為synchronized,那么執行這個方法時就需要給當前對象 this 加鎖。對象只有一個,而synchronized方法可能有多個,因此,如果某個線程已經獲得了這把鎖,那么就意味著其他線程不僅沒有權限執行當前方法,就連其他同步方法也無法執行,因為這些同步方法都需要獲得同一個對象鎖。
想要證明是鎖對象其實非常簡單,下面這個demo可以看出synchronized鎖定的是同一個共享對象,而不是這個對象的某一個加鎖方法。
import java.util.concurrent.TimeUnit;/*** 此例證明了synchronized鎖是鎖定的對象而不是代碼片段 <br>* 類名:ObjectLockDemo<br>* 作者: mht<br>* 日期: 2018年8月27日-下午1:25:47<br>*/ public class ObjectLockDemo {public static void main(String[] args) {T t = new T();new Thread(() -> t.doSomthing(), "t1").start();new Thread(() -> t.doOtherthing(), "t2").start();} }class T {synchronized void doSomthing() {System.out.println(Thread.currentThread().getName() + " do something start...");try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " do something end...");}synchronized void doOtherthing() {System.out.println(Thread.currentThread().getName() + " do otherthing...start");System.out.println(Thread.currentThread().getName() + " do otherthing...end");} }上面這個例子非常簡單。首先,我們為T類創建了兩個加鎖的方法doSomthing()和doOtherthing(),在主線程中,我們創建了一個 t 對象,然后通過線程 t1 調用 對象 t 的doSomthing()加鎖方法,緊接著線程 t2 去調用 對象 t 的另一個加鎖方法doOtherthing(),如果synchronized鎖住的是方法,那么在 t1 執行的5秒內,t2 可以自由的執行doOtherthing()方法而不受限制,但是如果是鎖住了 對象那么線程 t2 只有當 t1 執行完成后釋放了對象鎖,才能去執行 doOtherthing()方法,也就證明了synchronized鎖住的是 t 對象。
執行結果如下,很明顯,t2 等到 t1 完成后才執行了線程,synchronized鎖住的是對象。
二、synchronized用法
synchronized不論如何使用,都是直接鎖住了對象,但是在寫法上,卻存在很多變式:
用法一:
public class A {private Object o = new Object();private int count = 10;public void m() {synchronized (o) {// 任何線程若想執行代碼塊中的語句,必須先拿到o的鎖count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}} }用法二:
public class A {private int count = 10;public void m() {synchronized (this) {// 任何線程若想執行代碼塊中的語句,必須先拿到this的鎖count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}} }synchronized鎖定的是一個對象,任何代表對象的事物都可以作為synchronized參數。
用法三:
public class A {private int count = 10;public synchronized void m() { // 等同于在方法的代碼執行時要synchronized(this)count--;System.out.println(Thread.currentThread().getName() + " count = " + count);} }上面代碼就是平時比較常見的加鎖方式,雖然synchronized修飾了方法,但是一定不能理解為鎖定了代碼塊,而是執行這段代碼的當前對象 this
用法四(修飾靜態方法):
public class A {private static int count = 10;public synchronized static void m() { // 這里相當于synchronized(thingking.com.A.class)count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void mm() {synchronized (A.class) {// 這是不可以寫this,this指代對象,而靜態方法是通過類來調用的,沒有this對象count--;}} }三、互斥鎖的重入性
作為一種互斥鎖,synchronized可以避免并發情景下的線程亂入問題,保證了同步代碼塊的順序執行。
重入性指的是,允許同一個線程重復獲得已經獲得的鎖,不會造成阻塞,即重復進入同步代碼塊。重入性是互斥鎖的一個重要特性,如果在同步代碼塊中又調用了該對象的其他同步方法,那么就會出現當前線程需要反復獲取鎖的情況,如果鎖不允許同一個線程重入,就會出現“我拿不到我自己占用的鎖”的尷尬情況,最終導致死鎖!
public class T {synchronized void m() {System.out.println("m start...");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m end...");}public static void main(String[] args) {new TT().m();} }class TT extends T {@Overridesynchronized void m() {System.out.println("child m start...");super.m();System.out.println("child m end...");} }上述代碼是重入性的一種典型應用,子類中的同步方法調用父類中的同步方法,也是允許重入的。當調用子類的同步方法時,實際上鎖定的是this,而子類的對象同樣也可以看做是一個父類對象,因此這個鎖是允許重入的,不會造成死鎖。
執行結果:
四、注意異常
在默認情況下,synchronized鎖在發生異常時會自動釋放鎖。所以在并發處理的過程中,有異常要多加小心,不然可能會發生不一致的情況。
比如,在一個web app處理過程中,多個servlet 線程共同訪問同一個資源,這是如果如果異常處理不合適,在第一個線程中拋出異常,其他線程會進入同步代碼塊,有可能會訪問到異常產生的數據,因此要非常小心的處理同步業務邏輯中的異常。
public class T {int count = 0;synchronized void m() {System.out.println(Thread.currentThread().getName() + " start...");while (true) {count++;System.out.println(Thread.currentThread().getName() + " count = " + count);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}if (count == 5) {int i = 1 / 0;// 此處拋出異常,鎖將被釋放,要想不釋放,可以在這里進行catch,然后讓循環繼續}}}public static void main(String[] args) {T t = new T();Runnable r = new Runnable() {@Overridepublic void run() {t.m();}};new Thread(r, "t1").start();try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}new Thread(r, "t2").start();} }執行結果:
五、鎖的優化與升級
5.1 synchronized實現原理
Java 語言中為每個引用對象實例都設置了一把鎖,JVM基于進入和退出 monitor 對象來實現同步的。
monitor對象是與對象實例相關聯的一個鎖對象。
當代碼塊被synchronized修飾后,編譯器會在同步代碼塊開始的位置插入一條 monitorenter 指令,代表進入monitor 對象;同時會在同步塊結束的位置插入一條monitorexit指令,代表退出 monitor 對象。
JVM保證每個monitorenter都有一個monitorexit 與之匹配。任何對象都有一個monitor與之關聯,并且一個monitor被持有后,它將處于鎖定狀態。
5.2 偏向鎖
在synchronized設計之初,都是通過上面的邏輯來實現同步代碼的,這也導致了性能上被很多人詬病。
隨著研究的深入,人們發現,在很多場景下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。
為了避免這種情況下去加鎖,JVM會在線程第一次獲得鎖的時候,在對象頭上標記線程id以及鎖狀態,在下次請求鎖的時候,偏向于當前線程。
5.3 自旋鎖
當存在兩條以上的線程請求鎖時,線程會嘗試通過循環來嘗試請求鎖,舊的虛擬機實現上是循環10次,現在的JVM會更加智能地判斷循環次數。
總之,當線程需要獲得鎖而鎖已經被占用的情況下,線程就會自旋等待,由于自旋本質上就是 while循環,是一種線程運行狀態,因此會消耗CPU資源。
如果同步代碼塊很小,競爭鎖的線程個數不多,那么自旋鎖在一定程度上也可以實現重量級鎖的優化。
5.4 重量級鎖
重量級鎖是即synchronized的最終實現,它需要請求操作系統為對象上鎖,保證同步代碼塊的有序執行。
鎖升級的過程是 無鎖-->偏向鎖-->自旋鎖-->重量級鎖,隨著并發增大,同步代碼塊的執行時間變長,鎖的“重量”會逐步升級,且不可逆。
?
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Java内置锁——synchronized的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++保留小数点后三位数_C++保留有效
- 下一篇: uds帧格式_如何看懂UDS诊断报文