Java之synchronized(含與ReentrantLock的區(qū)別解讀)
1. synchronized與ReentrantLock的區(qū)別
區(qū)別點(diǎn) | synchronized | ReentrantLock |
---|---|---|
是什么? | 關(guān)鍵字,是 JVM 層面通過(guò)監(jiān)視器實(shí)現(xiàn)的 | 類,基于 AQS 實(shí)現(xiàn)的 |
公平鎖與否? | 非公平鎖 | 支持公平鎖和非公平鎖,默認(rèn)非公平鎖 |
獲取當(dāng)前線程是否上鎖 | 無(wú) | 可以(isHeldByCurrentThread()) |
條件變量 | 無(wú) | 支持條件變量(newCondition()) |
異常處理 | 在 synchronized 塊中發(fā)生異常,鎖會(huì)自動(dòng)釋放 | 在 ReentrantLock 中沒(méi)有在 finally 塊中正確地調(diào)用 unlock() 方法,則可能會(huì)導(dǎo)致死鎖 |
靈活性1 | 自動(dòng)加鎖和釋放鎖 | 手動(dòng)加鎖和釋放鎖 |
靈活性2 | 無(wú) | 允許嘗試去獲取鎖而不阻塞(如 tryLock 方法),并且可以指定獲取鎖等待的時(shí)間(如 tryLock(long time, TimeUnit unit))。 |
可中斷性 | 不可中斷,除非發(fā)生了異常 | 允許線程中斷另一個(gè)持有鎖的線程,這樣持有鎖的線程可以選擇放棄鎖并響應(yīng)中斷。1.tryLock(long timeout, TimeUnit unit);2.lockInterruptibly()和interrupt()配合使用 |
鎖的內(nèi)容 | 對(duì)象,鎖信息保存在對(duì)象頭中 | int類型的變量來(lái)標(biāo)識(shí)鎖的狀態(tài):private volatile int state; |
鎖升級(jí)過(guò)程 | 無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖 | 無(wú) |
使用位置 | 普通方法、靜態(tài)方法、代碼塊 | 代碼塊(方法里的代碼,初始化塊都是代碼塊) |
2. synchronized的作用
在Java中,使用synchronized關(guān)鍵字可以確保任何時(shí)刻只有一個(gè)線程可以執(zhí)行特定的方法或者代碼塊。這有助于防止數(shù)據(jù)競(jìng)爭(zhēng)條件(race conditions)和其他由于線程間共享資源而產(chǎn)生的問(wèn)題。
當(dāng)一個(gè)方法或代碼塊被聲明為synchronized,它意味著在該方法或代碼塊執(zhí)行期間,其他試圖獲得相同鎖的線程將被阻塞,直到持有鎖的線程釋放該鎖。這個(gè)鎖通常是對(duì)象的一個(gè)監(jiān)視器(monitor),對(duì)于靜態(tài)方法來(lái)說(shuō)是類的Class對(duì)象,對(duì)于實(shí)例方法則是擁有該方法的對(duì)象。
synchronized可以限制對(duì)共享資源的訪問(wèn),它鎖定的并不是臨界資源,而是某個(gè)對(duì)象,只有線程獲取到這個(gè)對(duì)象的鎖才能訪問(wèn)臨界區(qū),進(jìn)而訪問(wèn)臨界區(qū)中的資源。
保證線程安全。
當(dāng)多個(gè)線程去訪問(wèn)同一個(gè)類(對(duì)象或方法)的時(shí)候,該類都能表現(xiàn)出正常的行為(與自己預(yù)想的結(jié)果一致),那我們就可以說(shuō)這個(gè)類是線程安全的。
造成線程安全問(wèn)題的主要誘因有兩點(diǎn)
- 存在共享數(shù)據(jù)(也稱臨界資源)
- 存在多條線程共同操作共享數(shù)據(jù)
當(dāng)存在多個(gè)線程操作共享數(shù)據(jù)時(shí),需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式有個(gè)高尚的名稱叫互斥鎖,即能達(dá)到互斥訪問(wèn)目的的鎖,也就是說(shuō)當(dāng)一個(gè)共享數(shù)據(jù)被當(dāng)前正在訪問(wèn)的線程加上互斥鎖后,在同一個(gè)時(shí)刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。
在 Java 中,關(guān)鍵字 synchronized可以保證在同一個(gè)時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對(duì)方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized另外一個(gè)重要的作用,synchronized可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見(jiàn)性,完全可以替代volatile功能)。
3. synchronized的使用
下面三種本質(zhì)上都是鎖對(duì)象
3.1 修飾實(shí)例方法
作用于當(dāng)前實(shí)例,進(jìn)入同步代碼前需要先獲取實(shí)例的鎖
- 示例代碼:
public class SynchronizedDemo2 { int num = 0; public synchronized void add() { // public void add() { for (int i = 0; i < 10000; i++) { num++; } } public static class AddDemo extends Thread { private SynchronizedDemo2 synchronizedDemo2; public AddDemo(SynchronizedDemo2 synchronizedDemo2) { this.synchronizedDemo2 = synchronizedDemo2; } @Override public void run() { this.synchronizedDemo2.add(); } } public static void main(String[] args) throws InterruptedException { // 要想拿到臨界資源,就必須先獲得到這個(gè)對(duì)象的鎖。 SynchronizedDemo2 synchronizedDemo2 = new SynchronizedDemo2(); AddDemo addDemo1 = new AddDemo(synchronizedDemo2); AddDemo addDemo2 = new AddDemo(synchronizedDemo2); AddDemo addDemo3 = new AddDemo(synchronizedDemo2); addDemo1.start(); addDemo2.start(); addDemo3.start(); // 阻塞主線程 addDemo1.join(); addDemo2.join(); addDemo3.join(); // 打印結(jié)果 System.out.println(synchronizedDemo2.num); } }
- 打印:
期望結(jié)果:30000
無(wú)synchronized結(jié)果:23885
有synchronized結(jié)果:30000
synchronize作用于實(shí)例方法需要注意:
- 實(shí)例方法上加synchronized,線程安全的前提是,多個(gè)線程操作的是同一個(gè)實(shí)例,如果多個(gè)線程作用于不同的實(shí)例,那么線程安全是無(wú)法保證的
- 同一個(gè)實(shí)例的多個(gè)實(shí)例方法上有synchronized,這些方法都是互斥的,同一時(shí)間只允許一個(gè)線程操作同一個(gè)實(shí)例的其中的一個(gè)synchronized方法
3.2 修飾靜態(tài)方法
作用于類的Class對(duì)象,進(jìn)入修飾的靜態(tài)方法前需要先獲取類的Class對(duì)象的鎖
鎖定靜態(tài)方法需要通過(guò)類.class,或者直接在靜態(tài)方法上加上關(guān)鍵字。但是,類.class不能使用this來(lái)代替。
注:在同一個(gè)類加載器中,class是單例的,這也就能保證synchronized能夠只讓一個(gè)線程訪問(wèn)臨界資源。
- 示例代碼:
public class SynchronizedDemo1 { static int num = 0; // 加上synchronized保證線程安全 public static synchronized void add() { // public static void add() { for (int i = 0; i < 10000; i++) { num++; } } // 同上 public static void add1() { synchronized (SynchronizedDemo1.class) { for (int i = 0; i < 10000; i++) { num++; } } } public static class AddDemo extends Thread { @Override public void run() { SynchronizedDemo1.add(); } } public static void main(String[] args) throws InterruptedException { AddDemo addDemo1 = new AddDemo(); AddDemo addDemo2 = new AddDemo(); AddDemo addDemo3 = new AddDemo(); addDemo1.start(); addDemo2.start(); addDemo3.start(); // 阻塞主線程 addDemo1.join(); addDemo2.join(); addDemo3.join(); // 打印結(jié)果 System.out.println(SynchronizedDemo1.num); } }
- 打印:
期望結(jié)果:30000
無(wú)synchronized結(jié)果:14207
有synchronized結(jié)果:30000
3.3 修飾代碼塊
需要指定加鎖對(duì)象(記做lockobj),在進(jìn)入同步代碼塊前需要先獲取lockobj的鎖
若是this,相當(dāng)于修飾實(shí)例方法
- 示例代碼:
public class SynchronizedDemo3 { private static Object lockobj = new Object(); private static int num = 0; public static void add() { synchronized (lockobj) { for (int i = 0; i < 10000; i++) { num++; } } } public static class AddDemo extends Thread { @Override public void run() { SynchronizedDemo3.add(); } } public static void main(String[] args) throws InterruptedException { AddDemo addDemo1 = new AddDemo(); AddDemo addDemo2 = new AddDemo(); AddDemo addDemo3 = new AddDemo(); addDemo1.start(); addDemo2.start(); addDemo3.start(); // 阻塞主線程 addDemo1.join(); addDemo2.join(); addDemo3.join(); // 打印結(jié)果 System.out.println(SynchronizedDemo3.num); } }
- 打?。?/li>
期望結(jié)果:30000
無(wú)synchronized結(jié)果:28278
有synchronized結(jié)果:> 示例代碼:
4. 分析代碼是否互斥
分析代碼是否互斥的方法,先找出synchronized作用的對(duì)象是誰(shuí),如果多個(gè)線程操作的方法中synchronized作用的鎖對(duì)象一樣,那么這些線程同時(shí)異步執(zhí)行這些方法就是互斥的。
- 示例代碼:
public class SynchronizedDemo4 { // 作用于當(dāng)前類的實(shí)例對(duì)象 public synchronized void m1() { } // 作用于當(dāng)前類的實(shí)例對(duì)象 public synchronized void m2() { } // 作用于當(dāng)前類的實(shí)例對(duì)象 public void m3() { synchronized (this) { } } // 作用于當(dāng)前類Class對(duì)象 public static synchronized void m4() { } // 作用于當(dāng)前類Class對(duì)象 public static void m5() { synchronized (SynchronizedDemo4.class) { } } public static class T extends Thread { SynchronizedDemo4 demo; public T(SynchronizedDemo4 demo) { this.demo = demo; } @Override public void run() { super.run(); } } public static void main(String[] args) { SynchronizedDemo4 d1 = new SynchronizedDemo4(); Thread t1 = new Thread(() -> { d1.m1(); }); Thread t2 = new Thread(() -> { d1.m2(); }); Thread t3 = new Thread(() -> { d1.m3(); }); SynchronizedDemo4 d2 = new SynchronizedDemo4(); Thread t4 = new Thread(() -> { d2.m2(); }); Thread t5 = new Thread(() -> { SynchronizedDemo4.m4(); }); Thread t6 = new Thread(() -> { SynchronizedDemo4.m5(); }); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); t6.start(); } }
結(jié)論:
- 線程t1、t2、t3中調(diào)用的方法都需要獲取d1的鎖,所以他們是互斥的
- t1/t2/t3這3個(gè)線程和t4不互斥,他們可以同時(shí)運(yùn)行,因?yàn)榍懊嫒齻€(gè)線程依賴于d1的鎖,t4依賴于d2的鎖
- t5、t6都作用于當(dāng)前類的Class對(duì)象鎖,所以這兩個(gè)線程是互斥的,和其他幾個(gè)線程不互斥
5. synchronized的可重入性
- 示例代碼:
public class SynchronizedDemo5 { synchronized void method1() { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } method2(); System.out.println("method1 thread-" + Thread.currentThread().getName() + " end"); } synchronized void method2() { try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("method2 thread-" + Thread.currentThread().getName() + " end"); } public static void main(String[] args) { SynchronizedDemo5 t5 = new SynchronizedDemo5(); new Thread(t5::method1, "1").start(); new Thread(t5::method1, "2").start(); new Thread(t5::method1, "3").start(); } }
- 打?。?/li>
method2 thread-1 end
method1 thread-1 end
method2 thread-3 end
method1 thread-3 end
method2 thread-2 end
method1 thread-2 end
- 結(jié)論:
當(dāng)線程啟動(dòng)的時(shí)候,已經(jīng)獲取了對(duì)象的鎖,等method1調(diào)用method2方法的時(shí)候,同樣是拿到了這個(gè)對(duì)象的鎖。所以synchronized是可重入的。
6. 發(fā)生異常synchronized會(huì)釋放鎖
- 示例代碼:
public class SynchronizedDemo6 { int num = 0; synchronized void add() { System.out.println("thread" + Thread.currentThread().getName() + " start"); while (num <= 7) { num++; System.out.println("thread" + Thread.currentThread().getName() + ", num is " + num); if (num == 3) { throw new NullPointerException(); } } } public static void main(String[] args) throws InterruptedException { SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6(); new Thread(synchronizedDemo6::add, "1").start(); Thread.sleep(1000); new Thread(synchronizedDemo6::add, "2").start(); } }
打?。?/p>
thread1 start
thread1, num is 1
thread1, num is 2
thread1, num is 3
Exception in thread “1” java.lang.NullPointerException
at com.xin.demo.threaddemo.lockdemo.synchronizeddemo.SynchronizedDemo6.add(SynchronizedDemo6.java:14)
at java.lang.Thread.run(Thread.java:748)
thread2 start
thread2, num is 4
thread2, num is 5
thread2, num is 6
thread2, num is 7
thread2, num is 8
- 結(jié)論:
發(fā)生異常synchronized會(huì)釋放鎖
7. synchronized的實(shí)現(xiàn)原理與應(yīng)用(包含鎖的升級(jí)過(guò)程)
我的另一篇讀書(shū)筆記:Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理
鎖的升級(jí)過(guò)程:無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖,詳細(xì)情況還是看上面這篇文章
- 無(wú)鎖
- 偏向鎖:在鎖對(duì)象的對(duì)象頭中記錄一下當(dāng)前獲取到該鎖的線程ID,該線程下次如果又來(lái)獲取該鎖就可以直接獲取到了,也就是支持鎖重入
- 輕量級(jí)鎖:當(dāng)兩個(gè)或以上線程交替獲取鎖,但并沒(méi)有在對(duì)象上并發(fā)的獲取鎖時(shí),偏向鎖升級(jí)為輕量級(jí)鎖。在此階段,線程采取CAS的自旋方式嘗試獲取鎖,避免阻塞線程造成的CPU在用戶態(tài)和內(nèi)核態(tài)間轉(zhuǎn)換的消耗。輕量級(jí)鎖時(shí),CPU是用戶態(tài)。
- 重量級(jí)鎖:兩個(gè)或以上線程并發(fā)的在同一個(gè)對(duì)象上進(jìn)行同步時(shí),為了避免無(wú)用自旋消耗CPU,輕量級(jí)鎖會(huì)升級(jí)成重量級(jí)鎖。重量級(jí)鎖時(shí),CPU是內(nèi)核態(tài)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot+Jersey跨域文件上傳的實(shí)現(xiàn)示例
在SpringBoot開(kāi)發(fā)后端服務(wù)時(shí),我們一般是提供接口給前端使用,本文主要介紹了SpringBoot+Jersey跨域文件上傳的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07java 對(duì)象參數(shù)去空格方式代碼實(shí)例
這篇文章主要介紹了java 對(duì)象參數(shù)去空格方式代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10Mybatis-plus apply函數(shù)使用場(chǎng)景分析
Mybatis-plus 里面的 apply方法 是用于拼接自定義的條件判斷,自定義時(shí)間查詢,根據(jù)傳進(jìn)來(lái)的開(kāi)始日期,查詢所有該日期是數(shù)據(jù),但是數(shù)據(jù)庫(kù)中保存是時(shí)間,所以需要使用apply查詢方式并格式化,這篇文章給大家介紹Mybatis-plus apply函數(shù)使用,感興趣的朋友一起看看吧2024-02-02Java8實(shí)現(xiàn)FTP及SFTP文件上傳下載
這篇文章主要介紹了Java8實(shí)現(xiàn)FTP及SFTP文件上傳下載,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09SpringBoot引入模板引擎實(shí)現(xiàn)視圖解析
這篇文章主要介紹了SpringBoot引入模板引擎實(shí)現(xiàn)視圖解析方法流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10Spring 開(kāi)發(fā)過(guò)程中Value 注解的使用場(chǎng)景
這篇文章主要介紹了Spring 開(kāi)發(fā)過(guò)程中Value 注解的使用場(chǎng)景,幫助大家更好的理解和使用spring框架,感興趣的朋友可以了解下2020-11-11前端發(fā)送的請(qǐng)求Spring如何返回一個(gè)文件詳解
這篇文章主要給大家介紹了關(guān)于前端發(fā)送的請(qǐng)求Spring如何返回一個(gè)文件的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-09-09Spring AI TikaDocumentReader詳解
TikaDocumentReader是SpringAI中用于從多種格式文檔中提取文本內(nèi)容的組件,支持PDF、DOC/DOCX、PPT/PPTX和HTML等格式,它在構(gòu)建知識(shí)庫(kù)、文檔處理和數(shù)據(jù)清洗等任務(wù)中非常有用2025-01-01Java自動(dòng)添加重寫(xiě)的toString方法詳解
在本篇文章里小編給大家整理了關(guān)于Java自動(dòng)添加重寫(xiě)的toString方法總結(jié),需要的朋友們學(xué)習(xí)下。2019-07-07