前言:
synchronized的簡介
Synchronized的用法就是為了避免因資源搶占導(dǎo)致的數(shù)據(jù)錯亂夜矗,從而讓線程進(jìn)行同步筝蚕,保證同步內(nèi)的對象只有一個線程來執(zhí)行值更改使用操作,是并發(fā)控制中必不可少的部分迎膜,懂的都懂书斜,今天來深扒一下它
synchronized的特性
原子性伦连、指一個操作或多個操作,要么全部執(zhí)行并且執(zhí)行的過程并不會被任何因素打斷袭异,要么就都不執(zhí)行钠龙,比如
int i=1;這個操作就是原子性要么執(zhí)行要么不執(zhí)行御铃,而i++就不是原子性的碴里,因為包含了讀取、計算上真、賦值幾步咬腋,原值可能還沒完成時就已經(jīng)被賦值了,而原子性保證了執(zhí)行過程不會被中斷
synchronized與volatile的最大區(qū)別就是原子性谷羞,volatile不具備原子性
可見性帝火、指多個線程訪問一個資源時,該資源的狀態(tài)湃缎、值信息等對于其他線程都是可見的犀填,比如
int i =1;這個值如果是全局變量嗓违,在synchronized內(nèi)執(zhí)行變化它時九巡,其他線程也可以讀取到當(dāng)前i的值
有序性、程序執(zhí)行的順序都是按照代碼先后順序執(zhí)行蹂季,由于Java允許編譯器和處理器對指令進(jìn)行重排冕广,但他并不影響單線程的順序,而是影響了多線程的并發(fā)執(zhí)行順序性偿洁,synchronized則保證了同步代碼塊內(nèi)是有順序的
可重入性撒汉、就是擁有了這個鎖還能重復(fù)申請
一、synchronized的使用
1.修飾實例方法
鎖的是當(dāng)前實例對象涕滋,進(jìn)入同步代碼獲得當(dāng)前實例
public synchronized void add(){...};
反編譯時睬辐,add方法的flags多了一個ACC_SYNCHRONIZED標(biāo)志,這標(biāo)志用來告訴JVM這是一個同步方法,在進(jìn)入該方法之前先獲取相應(yīng)的鎖溯饵,鎖的計數(shù)器加1侵俗,方法結(jié)束后計數(shù)器-1,如果獲取失敗就阻塞住丰刊,直到該鎖被釋放隘谣。
2.修飾靜態(tài)方法
鎖的是當(dāng)前類,進(jìn)入同步代碼前獲取當(dāng)前類對象
public static synchronized void add(){...};
3.同步代碼塊
鎖括號內(nèi)的對象或當(dāng)前類
public class Test{
? private Test instance = new Test();
? synchronized(instance);? //鎖對象
? synchronized(Test.class){...} //鎖類
}
反編譯時啄巧,同步代碼塊是由monitorenter指令進(jìn)入寻歧,然后monitorexit釋放鎖
二、為什么任何對象都可以作為鎖
詳見:JVM內(nèi)存模型中對象的組成結(jié)構(gòu)
在JVM 中每個對象分為三部分存在:對象頭棵帽、示例數(shù)據(jù)熄求、對齊填充
對象頭中又有MarkWorld(運(yùn)行時元數(shù)據(jù))
鎖狀態(tài)標(biāo)志中便記錄了加鎖的信息
Mark Word在不同的鎖狀態(tài)下存儲的內(nèi)容不同,在32位JVM中是這么存的:
偏向鎖逗概、輕量級鎖弟晚、重量級鎖的具體使用會在下面具體介紹區(qū)別和聯(lián)系
而查看對象頭信息
總結(jié)
由于每個對象的Mark Word中都有儲存鎖的信息,可以說 鎖是對象逾苫,任何對象都可以作為鎖
偏向鎖卿城、輕量級鎖、重量級鎖
偏向鎖
只有第一個申請鎖的線程會使用鎖铅搓,有其他線程競爭就膨脹為輕量級鎖
當(dāng)一個線程訪問同步塊并獲取鎖時瑟押,會在對象頭和棧幀中的所記錄里存儲鎖偏向的線程ID,以后該線程進(jìn)入和退出同步塊不需要CAS(Compare and Swap星掰,比較并替換)操作來爭奪鎖資源多望。對一個線程的偏向,如果有其他線程競爭才去釋放(再替換線程ID即可)氢烘,當(dāng)新線程發(fā)起替換對象頭中的線程ID為自身的CAS請求時怀偷,回去判斷擁有此偏向鎖的線程是否還活著,如果不活著播玖,則置位無鎖狀態(tài)椎工,如果或者則掛起線程,并將只想當(dāng)前線程的鎖記錄地址放入頭對象蜀踏,膨脹成輕量級鎖维蒙,然后恢復(fù)持有鎖的線程
ps:當(dāng)前線程掛起再恢復(fù)的過程中并沒有發(fā)聲鎖的轉(zhuǎn)移,只是“將頭對象中的線程ID變更為只想鎖記錄地址的指針”果覆,偏向鎖是在單線程執(zhí)行代碼塊時使用的機(jī)制颅痊,如果多線程并發(fā)時(線程A并未執(zhí)行完同步代碼塊,B線程發(fā)起了鎖的申請)局待,則一定會轉(zhuǎn)化為輕量級鎖或重量級鎖斑响。
輕量級鎖
一個線程自旋等待持有鎖線程釋放鎖吗讶,若自旋后還沒獲得膨脹為重量級鎖
每次線程想進(jìn)入同步代碼塊的時候,都得通過CAS嘗試將對象頭中的所指針替換為自身棧中的記錄恋捆,如果沒有成功,則進(jìn)入而自適應(yīng)的自旋(動態(tài)改變自旋等待次數(shù)重绷,有另外一個線程來競爭鎖時沸停,線程在原地循環(huán)等待而不是阻塞,獲得鎖的線程釋放后就立馬獲得鎖)昭卓,如果自適應(yīng)自旋轉(zhuǎn)時還沒有獲得鎖愤钾,則膨脹為重量鎖
重量級鎖
實際的多線程阻塞等待,切換鎖的過程
通過對象內(nèi)部的監(jiān)視器(monitor)實現(xiàn)候醒,其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實現(xiàn)能颁,操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高倒淫。
應(yīng)用場景
偏向鎖:無實際競爭伙菊,且將來只有第一個申請鎖的線程會使用鎖
輕量級鎖:無實際競爭,多個線程交替使用鎖敌土;允許短時間的所競爭
重量級鎖:有實際競爭镜硕,且所競爭時間長
三、synchronized的底層實現(xiàn)
每個對象都有一個監(jiān)視器鎖monitor 當(dāng)monitor被占用時就會處于鎖定狀態(tài)返干,執(zhí)行monitorenter指令時兴枯,嘗試獲取monitor的所有權(quán),過程如下:
前提:
ObjectMonitor中有兩個隊列矩欠,_WaitSet 和 _EntryList财剖,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程
ObjectMonitor成員變量
? // initialize the monitor, exception the semaphore, all other fields
? // are simple integers or pointers
? ObjectMonitor() {
? ? _header? ? ? = NULL;
? ? _count? ? ? ? = 0;=============》計數(shù)器
? ? _waiters? ? ? = 0,
? ? _recursions? = 0;
? ? _object? ? ? = NULL;
? ? _owner? ? ? ? = NULL;===========》所持有該對象的線程
? ? _WaitSet? ? ? = NULL;==============》等待線程集合
? ? _WaitSetLock? = 0 ;========》保護(hù)等待隊列簡單的自旋鎖
? ? _Responsible? = NULL ;
? ? _succ? ? ? ? = NULL ;
? ? _cxq? ? ? ? ? = NULL ;====》阻塞上entry上最近科大的縣城列表癌淮,該列表是由waitNode構(gòu)成躺坟,扮演者線程代理
? ? FreeNext? ? ? = NULL ;
? ? _EntryList? ? = NULL ;============》入口線程集合
? ? _SpinFreq? ? = 0 ;
? ? _SpinClock? ? = 0 ;
? ? OwnerIsThread = 0 ;
? ? _previous_owner_tid = 0;
? }
ObjectMonitor工作過程
1. 當(dāng)多一個線程訪問同一段同步代碼塊時,進(jìn)入_EntryList集合
2.當(dāng)線程獲取到對象的monitor后進(jìn)入_Owner區(qū)域该默,并把monitor中的owner變量設(shè)為當(dāng)前線程瞳氓,monitor是依賴于底層操作系統(tǒng)的mutex lock來實現(xiàn)互斥的,線程獲取mutex成功栓袖,則會持有該mutex這時候其他線程無法獲取該metux匣摘、并將monitor的count+1(此時表示當(dāng)前線程持有當(dāng)前對象并加鎖)
3.當(dāng)線程調(diào)用wait()方法后,釋放當(dāng)前持有的monitor既釋放所持有的mutex裹刮,owner變量恢復(fù)為null音榜,count-1,同時進(jìn)入_WaitSet集合等待調(diào)用notify/notifyAll被喚醒
4.若當(dāng)前線程執(zhí)行完畢捧弃,釋放當(dāng)前持有的monitor赠叼,owner變量恢復(fù)為null
總結(jié):同步鎖在這種實現(xiàn)方式中擦囊,因為Monitor是依賴于底層的操作系統(tǒng)實現(xiàn),這樣就存在用戶態(tài)和內(nèi)核態(tài)之間的切換嘴办,所以會增加性能的開銷
詳細(xì)流程圖
詳細(xì)代碼
頭文件:
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.hpp
實現(xiàn):
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp
代碼太長 詳細(xì)解析可看具體代碼解析
四瞬场、其他相關(guān)線程同步相關(guān)
Volatile實現(xiàn)原理
Volatile只能修飾變量,不能修飾方法或代碼塊
Volatile變量的可見性
Java虛擬機(jī)中定義了一種Java內(nèi)存模型(Java Memory Model涧郊,即JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異贯被,以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的并發(fā)效果,Java的內(nèi)存模型目標(biāo):定義程序中各個變量的訪問規(guī)則妆艘,既在虛擬機(jī)中將變量村存儲到內(nèi)存和從內(nèi)從中去出變量這樣的細(xì)節(jié)
而對于普通變量彤灶,線程A修改值后此時該值在此線程的工作內(nèi)存,尚未同步到主內(nèi)存時批旺,若常出現(xiàn)B線程使用此變量幌陕,此時拿到的是主內(nèi)存修改前的值,便發(fā)生了可見性不一致的問題
volatile可見性的實現(xiàn)就是借助了CPU的lock指令汽煮,通過在寫volatile的機(jī)器指令前加上lock前綴搏熄,使寫volatile具有以下兩個原則:
當(dāng)Volatile變量執(zhí)行寫操作后,JMM會把工作內(nèi)存中的最新變量值強(qiáng)制刷新到主內(nèi)存中
寫操作會導(dǎo)致其他線程中的緩存無效
這樣逗物,其他線程使用緩存時搬卒,發(fā)現(xiàn)本地工作內(nèi)存此變量無效,就會從主內(nèi)存獲取翎卓,這樣獲取到的就是最新的值契邀,實現(xiàn)了線程的可見性
Volatile變量的有序性
volatile是通過編譯器在生成字節(jié)碼時,在指令序列中添加“內(nèi)存屏障”來禁止指令重排序等
JVM的實現(xiàn)會在volatile讀寫前后均加上內(nèi)存屏障失暴,在一定程度上保證有序性坯门。如下所示:
LoadLoadBarrier
volatile 讀操作
LoadStoreBarrier
StoreStoreBarrier
volatile 寫操作
StoreLoadBarrier
Volatile的使用
public class TestVolatile {
? ? public static volatile int counter = 1;
? ? public static void main(String[] args){
? ? ? ? counter = 2;
? ? ? ? System.out.println(counter);
? ? }
}
字節(jié)碼層面
volatile在字節(jié)碼層面,就是使用訪問標(biāo)志:ACC_VOLATILE來表示
Volatile與synchronized區(qū)別
1.volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的逗扒,需要從主存中讀裙糯鳌;synchronized則是鎖定當(dāng)前變量矩肩,只有當(dāng)前線程可以訪問該變量现恼,其他線程被阻塞住黍檩;
2.volatile僅能使用在變量級別叉袍;synchronized則可以使用在變量、方法刽酱、和類級別的喳逛;
3.volatile僅能實現(xiàn)變量的修改可見性,不能保證原子性棵里;而synchronized則可以保證變量的修改可見性和原子性润文;
4.volatile不會造成線程的阻塞姐呐;synchronized可能會造成線程的阻塞;
Lock原理
Lock的使用
由于Lock是一個Java接口典蝌,所以需要new它的實現(xiàn)類常用的ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
private Lock lock = new ReentrantLock();
lock.lock();//獲取鎖
//do something
lock.unlock()//釋放鎖
lock.trylock()//獲取鎖 如果鎖被占有就放開
而lock的核心類是(AQS)AbstractQueuedSynchronizer 自旋鎖
synchronized與Lock的區(qū)別
五曙砂、各種名稱鎖總結(jié)
詳細(xì)介紹:https://tech.meituan.com/2018/11/15/java-lock.html
互斥鎖/讀寫鎖
互斥鎖在Java中的具體實現(xiàn)就是ReentrantLock。
讀寫鎖在Java中的具體實現(xiàn)就是ReadWriteLock骏掀。