Java開發(fā)中synchronized的定義及用法詳解
概念
是利用鎖的機(jī)制來實(shí)現(xiàn)同步的。
互斥性:即在同一時(shí)間只允許一個(gè)線程持有某個(gè)對(duì)象鎖,通過這種特性來實(shí)現(xiàn)多線程中的協(xié)調(diào)機(jī)制,這樣在同一時(shí)間只有一個(gè)線程對(duì)需同步的代碼塊(復(fù)合操作)進(jìn)行訪問?;コ庑晕覀円餐Q為操作的原子性。
可見性:必須確保在鎖被釋放之前,對(duì)共享變量所做的修改,對(duì)于隨后獲得該鎖的另一個(gè)線程是可見的(即在獲得鎖時(shí)應(yīng)獲得最新共享變量的值),否則另一個(gè)線程可能是在本地緩存的某個(gè)副本上繼續(xù)操作從而引起不一致。
用法
修飾靜態(tài)方法:
//同步靜態(tài)方法 public synchronized static void methodName() { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+" aaa"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(SynchronizedDemo::methodName).start(); } }
當(dāng)synchronized作用于靜態(tài)方法時(shí),其鎖就是當(dāng)前類的class對(duì)象鎖。由于靜態(tài)成員不專屬于任何一個(gè)實(shí)例對(duì)象,是類成員,因此通過class對(duì)象鎖可以控制靜態(tài) 成員的并發(fā)操作。需要注意的是如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非static synchronized方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對(duì)象,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖
修飾實(shí)例方法:
//同步非靜態(tài)方法 ,當(dāng)前線程的鎖便是實(shí)例對(duì)象methodName public synchronized void methodName() { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+" aaa"); } catch (InterruptedException e) { e.printStackTrace(); } }
當(dāng)一個(gè)線程正在訪問一個(gè)對(duì)象的 synchronized 實(shí)例方法,那么其他線程不能訪問該對(duì)象的其他 synchronized 方法,畢竟一個(gè)對(duì)象只有一把鎖,當(dāng)一個(gè)線程獲取了該對(duì)象的鎖之后,其他線程無法獲取該對(duì)象的鎖,所以無法訪問該對(duì)象的其他synchronized實(shí)例方法,但是其他線程還是可以訪問該實(shí)例對(duì)象的其他非synchronized方法,當(dāng)然如果是一個(gè)線程 A 需要訪問實(shí)例對(duì)象 obj1 的 synchronized 方法 f1(當(dāng)前對(duì)象鎖是obj1),另一個(gè)線程 B 需要訪問實(shí)例對(duì)象 obj2 的 synchronized 方法 f2(當(dāng)前對(duì)象鎖是obj2),這樣是允許的,因?yàn)閮蓚€(gè)實(shí)例對(duì)象鎖并不同相同。此時(shí)如果兩個(gè)線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,遺憾的是如果兩個(gè)線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了。
代碼塊方式(this):
//修飾非靜態(tài)方法 public void methodName() { //修飾代碼塊,this=當(dāng)前對(duì)象(誰調(diào)用就指待誰) synchronized (this) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " aaa"); } catch (InterruptedException e) { e.printStackTrace(); } } }
synchronized(this|object) {}:在 Java 中,每個(gè)對(duì)象都會(huì)有一個(gè) monitor 對(duì)象,這個(gè)對(duì)象其實(shí)就是 Java 對(duì)象的鎖,通常會(huì)被稱為“內(nèi)置鎖”或“對(duì)象鎖”。類的對(duì)象可以有多個(gè),所以每個(gè)對(duì)象有其獨(dú)立的對(duì)象鎖,互不干擾。
代碼塊方式(Class):
//修飾非靜態(tài)方法 public void methodName() { //修飾代碼塊,使用Class類 //使用ClassLoader 加載字節(jié)碼的時(shí)候會(huì)向堆里面存放Class類,所有的對(duì)象都對(duì)應(yīng)唯一的Class類 //SynchronizedDemo.class 這里拿到的就是堆里面的Class類,也就是所有的Class的對(duì)象都共同使用這個(gè)synchronized synchronized (SynchronizedDemo.class) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " aaa"); } catch (InterruptedException e) { e.printStackTrace(); } } }
synchronized(類.class) {}:在 Java 中,針對(duì)每個(gè)類也有一個(gè)鎖,可以稱為“類鎖”,類鎖實(shí)際上是通過對(duì)象鎖實(shí)現(xiàn)的,即類的 Class 對(duì)象鎖。每個(gè)類只有一個(gè) Class 對(duì)象,所以每個(gè)類只有一個(gè)類鎖。
在 Java 中,每個(gè)對(duì)象都會(huì)有一個(gè) monitor 對(duì)象,監(jiān)視器。
- 某一線程占有這個(gè)對(duì)象的時(shí)候,先monitor 的計(jì)數(shù)器是不是0,如果是0還沒有線程占有,這個(gè)時(shí)候線程占有這個(gè)對(duì)象,并且對(duì)這個(gè)對(duì)象的monitor+1;如果不為0,表示這個(gè)線程已經(jīng)被其他線程占有,這個(gè)線程等待。當(dāng)線程釋放占有權(quán)的時(shí)候,monitor-1;
- 同一線程可以對(duì)同一對(duì)象進(jìn)行多次加鎖,+1,+1,重入性。
堆棧分析:
jconsole.exe
JVM指令分析
Javap -V 反編譯
Monitorenter 互斥入口 Monitorexit 互斥出口
monitorexit有兩個(gè),一個(gè)是正常出口,一個(gè)是異常出口
以上是對(duì)代碼塊加鎖
ACC_SYNCHRONIZED : 對(duì)方法加鎖。加鎖標(biāo)記
java虛擬機(jī)對(duì)synchronized的優(yōu)化
對(duì)象頭與monitor:
一個(gè)漢字兩個(gè)字節(jié),一個(gè)字節(jié)8bit
實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對(duì)齊。
填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊。
對(duì)象頭:它實(shí)現(xiàn)synchronized的鎖對(duì)象的基礎(chǔ),一般而言,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里的,jvm中采用2個(gè)字來存儲(chǔ)對(duì)象頭(如果對(duì)象是數(shù)組則會(huì)分配3個(gè)字,多出來的1個(gè)字記錄的是數(shù)組長度),其主要結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成
虛擬機(jī)位數(shù) | 頭對(duì)象結(jié)構(gòu) | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息 |
32/64bit | Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個(gè)指針確定該對(duì)象是哪個(gè)類的實(shí)例 |
其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode、分代年齡、鎖標(biāo)記位等以下是32位JVM的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu).
偏向鎖:偏向鎖是Java 6之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作,耗時(shí))的代價(jià)而引入偏向鎖。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖。但是對(duì)于鎖競爭比較激烈的場合,偏向鎖就失效了,因?yàn)檫@樣場合極有可能每次申請(qǐng)鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會(huì)得不償失,需要注意的是,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。下面我們接著了解輕量級(jí)鎖。。
輕量級(jí)鎖:倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6之后加入的),此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖能夠提升程序性能的依據(jù)是“對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是,輕量級(jí)鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時(shí)間訪問同一鎖的場合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
重量級(jí)鎖:synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10,其中指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實(shí)現(xiàn)的。
自旋鎖:輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長,如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒辦法也就只能升級(jí)為重量級(jí)鎖了。
鎖消除:消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請(qǐng)求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會(huì)自動(dòng)將其鎖消除。
synchronized的可重入性
從互斥鎖的設(shè)計(jì)上來說,當(dāng)一個(gè)線程試圖操作一個(gè)由其他線程持有的對(duì)象鎖的臨界資源時(shí),將會(huì)處于阻塞狀態(tài),但當(dāng)一個(gè)線程再次請(qǐng)求自己持有對(duì)象鎖的臨界資源時(shí),這種情況屬于重入鎖,請(qǐng)求將會(huì)成功,在java中synchronized是基于原子性的內(nèi)部鎖機(jī)制,是可重入的,因此在一個(gè)線程調(diào)用synchronized方法的同時(shí)在其方法體內(nèi)部調(diào)用該對(duì)象另一個(gè)synchronized方法,也就是說一個(gè)線程得到一個(gè)對(duì)象鎖后再次請(qǐng)求該對(duì)象鎖,是允許的,這就是synchronized的可重入性。
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; static int j=0; @Override public void run() { for(int j=0;j<1000000;j++){ //this,當(dāng)前實(shí)例對(duì)象鎖 synchronized(this){ i++; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
正如代碼所演示的,在獲取當(dāng)前實(shí)例對(duì)象鎖后進(jìn)入synchronized代碼塊執(zhí)行同步代碼,并在代碼塊中調(diào)用了當(dāng)前實(shí)例對(duì)象的另外一個(gè)synchronized方法,再次請(qǐng)求當(dāng)前實(shí)例鎖時(shí),將被允許,進(jìn)而執(zhí)行方法體代碼,這就是重入鎖最直接的體現(xiàn),需要特別注意另外一種情況,當(dāng)子類繼承父類時(shí),子類也是可以通過可重入鎖調(diào)用父類的同步方法。注意由于synchronized是基于monitor實(shí)現(xiàn)的,因此每次重入,monitor中的計(jì)數(shù)器仍會(huì)加1.
到此這篇關(guān)于Java開發(fā)中synchronized的定義及用法詳解的文章就介紹到這了,更多相關(guān)Java synchronized內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
eclipse修改jvm參數(shù)調(diào)優(yōu)方法(2種)
本篇文章主要介紹了eclipse修改jvm參數(shù)調(diào)優(yōu)方法(2種),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02SpringBoot使用Mybatis&Mybatis-plus文件映射配置方法
這篇文章主要介紹了SpringBoot使用Mybatis&Mybatis-plus文件映射配置方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05Spring Boot集成mongodb數(shù)據(jù)庫過程解析
這篇文章主要介紹了Spring Boot集成mongodb數(shù)據(jù)庫過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05解決IDEA集成Docker插件后出現(xiàn)日志亂碼的問題
這篇文章主要介紹了解決IDEA集成Docker插件后出現(xiàn)日志亂碼的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11ThreadLocal線程在Java框架中的應(yīng)用及原理深入理解
這篇文章主要介紹了ThreadLocal在Java框架中的應(yīng)用及原理深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java網(wǎng)絡(luò)編程UDP實(shí)現(xiàn)多線程在線聊天
這篇文章主要為大家詳細(xì)介紹了Java網(wǎng)絡(luò)編程UDP實(shí)現(xiàn)多線程在線聊天,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07Java設(shè)計(jì)模式之原型模式(Prototype模式)介紹
這篇文章主要介紹了Java設(shè)計(jì)模式之原型模式(Prototype模式)介紹,本文講解了如何使用原型模式并給出了代碼實(shí)例,需要的朋友可以參考下2015-03-03