淺析Java關(guān)鍵詞synchronized的使用
1 引入Synchronized
- Synchronized是java虛擬機(jī)為線程安全而引入的。
- 互斥同步是一種最常見(jiàn)的并發(fā)正確性的保障手段。同步是指在多個(gè)線程并發(fā)訪問(wèn)共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一條線程使用。
- synchronized是最基本的互斥同步手段,它是一種塊結(jié)構(gòu)的同步語(yǔ)法。
- synchronized修飾代碼塊,無(wú)論該代碼塊正常執(zhí)行完成還是發(fā)生異常,都會(huì)釋放鎖
synchronized對(duì)線程訪問(wèn)的影響:
- 被synchronized修飾的同步塊在持有鎖的線程執(zhí)行完畢并釋放鎖之前,會(huì)阻塞其他線程的進(jìn)入。
- 被synchronized修飾的同步塊對(duì)同一條線程是可重入的
2 Synchronized的使用
可以作用在方法上或者方法里的代碼塊:
- 修飾方法,包括實(shí)例方法和靜態(tài)方法
- 修飾方法里的代碼塊,這時(shí)需要一個(gè)引用作為參數(shù)。
- Synchronized作用地方不同,產(chǎn)生的鎖類(lèi)型也不同,分為對(duì)象鎖和類(lèi)鎖
2.1 對(duì)象鎖
Synchronized修飾實(shí)例方法或者代碼塊(鎖對(duì)象不是*.class),此時(shí)生產(chǎn)對(duì)象鎖。多線程訪問(wèn)該類(lèi)的同一個(gè)對(duì)象的sychronized塊是同步的,訪問(wèn)不同對(duì)象不受同步限制。
2.1.1 Synchronized修飾實(shí)例方法
public static void main(String[] args){ TempTest tempTest = new TempTest(); Thread t1 = new Thread(() -> { tempTest.doing(Thread.currentThread().getName()); }); Thread t2 = new Thread(() -> { tempTest.doing(Thread.currentThread().getName()); }); t1.start(); t2.start(); } //同一時(shí)刻只能被一個(gè)線程調(diào)用 private synchronized void doing(String threadName){ for(int i=0;i<3;i++){ System.out.println("current thread is : "+threadName); try { Thread.sleep(50); } catch (InterruptedException e) {} } }
運(yùn)行結(jié)果:
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1
2.1.2 Synchronized修飾代碼塊
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { // 同步代碼塊形式:鎖為this,兩個(gè)線程使用的鎖是一樣的,線程1必須要等到線程0釋放了該鎖后,才能執(zhí)行 synchronized (this) { 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(); } }
運(yùn)行結(jié)果:
我是線程Thread-0
Thread-0結(jié)束
我是線程Thread-1
Thread-1結(jié)束
2.2 類(lèi)鎖
synchronize修飾靜態(tài)方法或指定鎖對(duì)象為Class,此時(shí)產(chǎn)生類(lèi)鎖。多線程訪問(wèn)該類(lèi)的所有對(duì)象的sychronized塊是同步的,
2.2.1 synchronize修飾靜態(tài)方法
public static void main(String[] args){ TempTest tempTest1 = new TempTest(); TempTest tempTest2 = new TempTest(); //雖然創(chuàng)建了兩個(gè)TempTest實(shí)例,但是依然是調(diào)用同一個(gè)doing方法(因?yàn)槭莻€(gè)static);因此doing還是會(huì)依次執(zhí)行 Thread t1 = new Thread(() -> tempTest1.doing(Thread.currentThread().getName())); Thread t2 = new Thread(() -> tempTest2.doing(Thread.currentThread().getName())); t1.start(); t2.start(); } //修飾靜態(tài)方法,則是類(lèi)鎖; private static synchronized void doing(String threadName){ for(int i=0;i<3;i++){ System.out.println("current thread is : "+threadName); try { Thread.sleep(50); } catch (InterruptedException e) {} } }
運(yùn)行結(jié)果:有序輸出 【如果去掉static ,則線程會(huì)交替執(zhí)行doing】
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1
2.2.2 synchronize指定鎖對(duì)象為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() + "結(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é)束
3 Synchronized原理分析
3.1 虛擬機(jī)如何辨別和處理synchronized
虛擬機(jī)可以從常量池中的方法表結(jié)構(gòu)中的ACC_ SYNCHRONIZED訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否是同步方法。
當(dāng)調(diào)用方法時(shí),調(diào)用指令將會(huì)檢查方法的ACC_ SYNCHRONIZED訪問(wèn)標(biāo)志是否設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有同步鎖,然后執(zhí)行方法,最后在方法完成時(shí)釋放同步鎖。
在方法執(zhí)行期間,執(zhí)行線程持有了同步鎖,其他任何線程都無(wú)法再獲得同一個(gè)鎖。
如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無(wú)法處理此異常,那這個(gè)同步方法所持有的鎖將在異常拋到同步方法之外時(shí)自動(dòng)釋放。
3.2 虛擬機(jī)對(duì)synchronized的編譯處理
以下代碼:
public class Foo { void onlyMe(Foo f) { synchronized(f) { doSomething(); } } private void doSomething(){ } }
編譯后,這段代碼生成的字節(jié)碼序列如下:
- synchronized關(guān)鍵字經(jīng)過(guò)Javac編譯之后,會(huì)在同步塊的前后生成monitorenter和monitorexit兩個(gè)字節(jié)碼指令。
- 指令含義:monitorenter:獲取對(duì)象的鎖;monitorexit:釋放對(duì)象的鎖
- 執(zhí)行monitorenter指令時(shí),首先嘗試獲取對(duì)象的鎖。如果對(duì)象沒(méi)被鎖定,或者當(dāng)前線程已經(jīng)持有了對(duì)象的鎖,就把鎖的計(jì)數(shù)器的值增加1
- 執(zhí)行monitorexit指令時(shí),將鎖計(jì)數(shù)器的值減1,一旦計(jì)數(shù)器的值為零,鎖隨即就被釋放
- 如果獲取對(duì)象鎖失敗,那當(dāng)前線程阻塞等待,直到鎖被釋放。
- 為了保證在方法異常完成時(shí)monitorenter和monitorexit指令依然可以正確配對(duì)執(zhí)行,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理程序,它的目的就是用來(lái)執(zhí)行monitorexit指令。
3.3 虛擬機(jī)執(zhí)行加鎖和釋放鎖的過(guò)程
那么重點(diǎn)來(lái)了到這里,有幾個(gè)問(wèn)題需明確:
- 什么叫對(duì)象的鎖?
- 如何確定鎖被線程持有?
- 執(zhí)行monitorenter后,對(duì)象發(fā)生什么變化?
- 鎖計(jì)數(shù)值保存在哪里,如何獲取到?
1. 什么叫對(duì)象的鎖?對(duì)象的內(nèi)存結(jié)構(gòu)參考文末補(bǔ)充內(nèi)容
- 鎖,一種可以被讀寫(xiě)的資源,對(duì)象的鎖是對(duì)象的一部分。
- 對(duì)象的結(jié)構(gòu)中有部分稱(chēng)為對(duì)象頭。
- 對(duì)象頭中有2bit空間,用于存儲(chǔ)鎖標(biāo)志,通過(guò)該標(biāo)志位來(lái)標(biāo)識(shí)對(duì)象是否被鎖定。
2. 如果確定鎖被線程持有?
- 代碼即將進(jìn)入同步塊的時(shí),如果鎖標(biāo)志位為“01”(對(duì)象未被鎖定),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄的空間,存儲(chǔ)鎖對(duì)象Mark Word的拷貝。(線程開(kāi)辟空間并存儲(chǔ)對(duì)象頭)
- 虛擬機(jī)將使用CAS操作嘗試把對(duì)象的Mark Word更新成指向鎖記錄的指針(對(duì)象頭的mw存儲(chǔ)指向線程“鎖記錄”中的指針)
- 如果CAS操作成功,即代表該線程擁有了這個(gè)對(duì)象的鎖,并且將對(duì)象的鎖標(biāo)志位轉(zhuǎn)變?yōu)?ldquo;00”
- 如果CAS操作失敗,那就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)獲取該對(duì)象的鎖。虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是,說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則就說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占。
- 解鎖過(guò)程:CAS操作把線程中保存的MW拷貝替換回對(duì)象頭中。假如能夠成功替換,那整個(gè)同步過(guò)程就順利完成了;如果替換失敗,則說(shuō)明有其他線程嘗試過(guò)獲取該鎖,就要在釋放鎖的同時(shí),喚醒被掛起的線程。
3 執(zhí)行monitorenter后,對(duì)象發(fā)生什么變化?
- 對(duì)象的鎖標(biāo)志位轉(zhuǎn)變?yōu)?ldquo;00”
- 擁有對(duì)象鎖的線程開(kāi)辟了新空間,保存了對(duì)象的Mark Word信息
- 對(duì)象的Mark Word保存了線程的鎖記錄空間的地址拷貝
4 鎖計(jì)數(shù)值保存在哪里?
我還沒(méi)搞懂。
monitorenter指令執(zhí)行的過(guò)程:
4 Synchronized與Lock
synchronized的缺陷
- 在多線程競(jìng)爭(zhēng)鎖時(shí),當(dāng)一個(gè)線程獲取鎖時(shí),它會(huì)阻塞所有正在競(jìng)爭(zhēng)的線程,這樣對(duì)性能帶來(lái)了極大的影響。
- 掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,上下文切換需要消耗很大性能。
- 效率低:鎖的釋放情況少,只有代碼執(zhí)行完畢或者異常結(jié)束才會(huì)釋放鎖;試圖獲取鎖的時(shí)候不能設(shè)定超時(shí),不能中斷一個(gè)正在使用鎖的線程,相對(duì)而言,Lock可以中斷和設(shè)置超時(shí)
- 不夠靈活:加鎖和釋放的時(shí)機(jī)單一,每個(gè)鎖僅有一個(gè)單一的條件(某個(gè)對(duì)象),相對(duì)而言,讀寫(xiě)鎖更加靈活
5 使用Synchronized有哪些要注意的
鎖對(duì)象不能為空,因?yàn)殒i的信息都保存在對(duì)象頭里
作用域不宜過(guò)大,影響程序執(zhí)行的速度,控制范圍過(guò)大,編寫(xiě)代碼也容易出錯(cuò)
在能選擇的情況下,既不要用Lock也不要用synchronized關(guān)鍵字,用java.util.concurrent包中的各種各樣的類(lèi),如果有必要,使用synchronized關(guān)鍵,因?yàn)榇a量少,避免出錯(cuò)
synchronized實(shí)際上是非公平的,新來(lái)的線程有可能立即獲得執(zhí)行,而在等待區(qū)中等候已久的線程可能再次等待,這樣有利于提高性能,但是也可能會(huì)導(dǎo)致饑餓現(xiàn)象。
知識(shí)補(bǔ)充
Java內(nèi)存層面的對(duì)象認(rèn)識(shí)
說(shuō)明:此分析基于HotSpot虛擬機(jī)
1 對(duì)象的創(chuàng)建
Java對(duì)象的創(chuàng)建方式有三種:
- 通過(guò)new創(chuàng)建
- 通過(guò)反序列化創(chuàng)建
- 通過(guò)復(fù)制創(chuàng)建
通過(guò)new方式的對(duì)象創(chuàng)建過(guò)程如下:
創(chuàng)建過(guò)程說(shuō)明:
- 執(zhí)行字節(jié)碼遇到new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到 一個(gè)類(lèi)的符號(hào)引用。
- 類(lèi)的初始化過(guò)程在后續(xù)章節(jié)詳細(xì)補(bǔ)充
- 給對(duì)象分配初始內(nèi)存空間有兩種方式:指針碰撞 和 空閑列表。
- 分配空間后,清空該段的【不包括對(duì)象頭】值,保證對(duì)象屬性的不設(shè)值就使用初始值
- 對(duì)象頭信息包括:元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡
- 執(zhí)行構(gòu)造函數(shù),給屬性初始化設(shè)置的值
2 對(duì)象的內(nèi)存布局
對(duì)象存儲(chǔ)的內(nèi)容分類(lèi)以及明細(xì)如下:
關(guān)于對(duì)象頭的補(bǔ)充說(shuō)明:
- 對(duì)象頭,在字長(zhǎng)為32位和64位的虛擬機(jī)中分別為32比特(4字節(jié))64比特(8字節(jié))。
- 對(duì)象頭的類(lèi)型指針:不一定所有對(duì)象都會(huì)存儲(chǔ)這項(xiàng)信息,意味著訪問(wèn)對(duì)象所屬的類(lèi)不一定通過(guò)對(duì)象自身。
- 如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。
關(guān)于實(shí)例數(shù)據(jù)的補(bǔ)充說(shuō)明:
- 對(duì)象屬性的存儲(chǔ)順序,受到虛擬機(jī)分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響
- 默認(rèn)的分配策略下:占據(jù)相同字節(jié)數(shù)的屬性會(huì)排列在一起,滿(mǎn)足該條件下,父類(lèi)的屬性排在前面。
關(guān)于對(duì)齊填充的說(shuō)明:
不一定會(huì)存在,因?yàn)?strong>對(duì)象的大小一定是8字節(jié)的整數(shù)倍,因此需要對(duì)齊填充這部分,充當(dāng)占位符
在32位字長(zhǎng)的虛擬機(jī)下,對(duì)象的內(nèi)存分布情況如下:
3 對(duì)象的訪問(wèn)定位
對(duì)象訪問(wèn)方式也是由虛擬機(jī)實(shí)現(xiàn)而定的,主流的訪問(wèn)方式主要有使用句柄和直接指針兩種:
3.1句柄訪問(wèn)
說(shuō)明:
句柄訪問(wèn)方式,Java堆中將可能會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自具體的地址
3.2 直接指針訪問(wèn)
說(shuō)明:
使用直接指針來(lái)訪問(wèn)最大的好處就是速度更快,只需要一次定位就能找到實(shí)例數(shù)據(jù),而句柄池則需要兩次:(需要先定位句柄池,再定位實(shí)例數(shù)據(jù))
以上就是淺析Java關(guān)鍵詞synchronized的使用的詳細(xì)內(nèi)容,更多關(guān)于Java關(guān)鍵詞synchronized的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java?synchronized同步關(guān)鍵字工作原理
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級(jí)鎖實(shí)現(xiàn)過(guò)程淺析
- Java synchronized重量級(jí)鎖實(shí)現(xiàn)過(guò)程淺析
- Java @Transactional與synchronized使用的問(wèn)題
- Java?synchronized與死鎖深入探究
- Java synchronized與CAS使用方式詳解
- synchronized及JUC顯式locks?使用原理解析
- java鎖synchronized面試常問(wèn)總結(jié)
- Java?HashTable與Collections.synchronizedMap源碼深入解析
- Java?Synchronized鎖的使用詳解
- AQS加鎖機(jī)制Synchronized相似點(diǎn)詳解
- Java必會(huì)的Synchronized底層原理剖析
- 一個(gè)例子帶你看懂Java中synchronized關(guān)鍵字到底怎么用
- 詳解Java?Synchronized的實(shí)現(xiàn)原理
- Synchronized?和?ReentrantLock?的實(shí)現(xiàn)原理及區(qū)別
- Java同步鎖synchronized用法的最全總結(jié)
相關(guān)文章
SpringBoot項(xiàng)目中遇到的BUG問(wèn)題及解決方法
這篇文章主要介紹了SpringBoot項(xiàng)目中遇到的BUG問(wèn)題及解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11SpringCloud實(shí)現(xiàn)灰度發(fā)布的方法步驟
本文主要介紹了SpringCloud實(shí)現(xiàn)灰度發(fā)布的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05詳解mybatis多對(duì)一關(guān)聯(lián)查詢(xún)的方式
這篇文章主要給大家介紹了關(guān)于mybatis多對(duì)一關(guān)聯(lián)查詢(xún)的相關(guān)資料,文中將關(guān)聯(lián)方式以及配置方式介紹的很詳細(xì),需要的朋友可以參考下2021-06-06Java后臺(tái)實(shí)現(xiàn)微信支付和微信退款
這篇文章主要介紹了Java后臺(tái)實(shí)現(xiàn)微信支付和微信退款,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Java進(jìn)行反編譯生成.java文件方式(javap、jad下載安裝使用)
這篇文章主要介紹了Java進(jìn)行反編譯生成.java文件方式(javap、jad下載安裝使用),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12Spring boot動(dòng)態(tài)修改日志級(jí)別的方法
我們經(jīng)常會(huì)遇到業(yè)務(wù)想看debug日志的問(wèn)題,但是debug日志頻繁打印會(huì)對(duì)日志查看有影響,且日志多對(duì)系統(tǒng)也會(huì)有一定的壓力,因此,如果可以在需要的時(shí)候動(dòng)態(tài)臨時(shí)調(diào)整下日志的級(jí)別則是比較完美的,spring boot已經(jīng)支持這種功能,需要的朋友可以參考下2022-12-12springboot將mybatis升級(jí)為mybatis-plus的實(shí)現(xiàn)
之前項(xiàng)目工程用的是mybatis,現(xiàn)在需要將其替換為mybatis-plus,本文主要介紹了springboot將mybatis升級(jí)為mybatis-plus的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09