專門做視頻的網(wǎng)站嗎網(wǎng)站seo分析常用的工具是
前言
之前兩篇文章介紹了線程的基本概念和鎖的基本知識,本文主要是學習同步機制,包括使用synchronized關鍵字、ReentrantLock等,了解鎖的種類,死鎖、競爭條件等并發(fā)編程中常見的問題。
一、關鍵字synchronized
- synchronied關鍵字可以把任意一個非null的對象當做鎖,屬于獨占式的悲觀鎖。同時屬于可重入鎖
- 早期的的synchronized屬于重量級的鎖,效率低下,因為監(jiān)視器是依賴底層的操作系統(tǒng)Lock實現(xiàn)的,從6之后java對sychronized進行了優(yōu)化,jdk1.6以后還引入了 大量的優(yōu)化,比如自旋鎖,適應性鎖,鎖消除,鎖粗化,偏向鎖,輕量級鎖等。
1.synchronized用法
常用來保證代碼的原子性,主要有三種使用方法
- 修飾實例:作用于當前的對象實例加鎖,進入同步代碼前獲得,當前對象實例的鎖。
synchronized void method() {
//業(yè)務代碼
}
- 修飾靜態(tài)方法: 也就是給當前類加鎖,會作用于該類所有的對象實例。如果線程A調(diào)用一個實例對象的非靜態(tài)synchronized方法,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài)synchronized方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為靜態(tài)synchronized方法是占用的鎖是當前類的鎖,而訪問非靜態(tài)synchronized方法占用的鎖是當前實例對象的鎖。
synchronized void staic method() {
//業(yè)務代碼
}
- 修飾代碼塊: 指定加鎖對象,對給定的對象/類加鎖,synchronized(this object)表示進入同步前要獲得給定對象的鎖,synchronized(類.class)表示進入同步前要獲得給定類class的鎖
synchronized(this) {
//業(yè)務代碼
}
2. synchronized實現(xiàn)原理
- 使用synchronized是不用我們?nèi)ゼ渔i和釋放lock,unlock,是jvm已經(jīng)代替去做了
- synchronized修飾代碼塊的時候,jvm是使用monitorenter和monitorexit兩個指令實現(xiàn)的(監(jiān)視器)
- 當修飾同步方法,jvm采用ACC_SYNCHRONIZED標記符來實現(xiàn)的同步, 這個標識表面了這是一個同步方法
3.synchronized鎖住的原理
monitorenter,monitorexit,ACC_SYNCHRONIZED都是基于monitor(監(jiān)視器)
所謂的Monitor其實是一種同步工具,也可以說是一種同步機制。在Java虛擬機(HotSpot)中,Monitor是由
ObjectMonitor實現(xiàn)的,可以叫做內(nèi)部鎖,或者Monitor鎖。
ObjectMonitor的工作原理:
ObjectMonitor有兩個隊列:WaitSet、EntryList,用來保存ObjectWaiter 對象列表。
_owner,獲取 Monitor 對象的線程進入 _owner 區(qū)時, _count + 1。如果線程調(diào)用了wait() 方法,此時會釋放Monitor 對象, _owner 恢復為空, _count - 1。同時該等待線程進入 _WaitSet 中,等待被喚醒。
-同步是鎖住的
- monitorenter,在判斷擁有同步標識 ACC_SYNCHRONIZED 搶先進入此方法的線程會優(yōu)先擁有 Monitor 的owner ,此時計數(shù)?+1。
- monitorexit,當執(zhí)行完退出后,計數(shù)?-1,歸 0 后被其他進入的線程獲得
4.除了原子性,synchronized的可見性和有序性,可重入性怎么實現(xiàn)
- 可見性:線程加鎖前,將清空工作內(nèi)存中共享變量的值,從而使用共享變量的時候,需要從主內(nèi)存中重新讀取最新的值。線程加鎖后,其他線程無法獲得主內(nèi)存中的共享變量的值,線程解鎖前必須把共享變量的最新值刷新到主內(nèi)存中。
- 有序性:synchronized同步的代碼塊具有排他性,一次只能被一個線程擁有,所以可以保證同一個時刻,代碼是單線程執(zhí)行的,因為as-if-serial存在,單線程語句是能夠保證最終結(jié)果是有序的,但是不保證不會進行指令重排,所以synchronized是保證有序是執(zhí)行結(jié)果的有序而不是防止指令重排的有序性。
- 可重入性:synchronized是可重入鎖,也就說允許一個線程二次請求自己持有的鎖的臨界資源,這種情況就是可重入鎖,鎖對象有個計數(shù)器,會記錄線程獲取鎖的次數(shù),當執(zhí)行完對應的代碼后,計數(shù)器就會減去1,只有歸零就會釋放鎖。之所以可以重入就是因為這個計數(shù)器。
5.synchronized和ReentrantLock的區(qū)別
可從鎖的實現(xiàn)、功能特點、性能維度等分析
-
鎖的實現(xiàn):synchronized是通過jvm實現(xiàn),是java的關鍵字;而reentrantlock是通過jdk層面的api實現(xiàn)的的(一般是lock()和unlock()方法配合try/catch/finally語句實現(xiàn))
-
性能:jdk1.6前synchronized性能比較差,應該都是要通過底層調(diào)用,但是1.6以后增加了適應性自旋,鎖消除等,兩者性能差不多。
-
功能特點:-
- ReentrantLock比synchronized增加了一些高級功能,如等待中斷,可實現(xiàn)公平鎖,可實現(xiàn)選擇性通知;
- synchronized只能是非公平鎖(內(nèi)部鎖),- ReentrantLock可以指定是公平還是非公平(公平鎖就是先等待的線程先獲得鎖);
- synchronized與wait()和notify()/notifyAll()方法結(jié)合實現(xiàn)等待/通知機制,ReentrantLock類借助Condition接口與newCondition()方法實現(xiàn)。
- ReentrantLock需要手工聲明來加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動釋放鎖
二、鎖類型
鎖可以分為
- 悲觀、樂觀鎖
- 獨享、共享鎖
- 互斥鎖、讀寫鎖
- 可重入鎖
- 公平鎖、非公平鎖
- 分段鎖
- 偏向鎖,輕量級鎖、重量級鎖
- 自旋鎖
以上是鎖的名詞,有的是指鎖的狀態(tài),有的是鎖特性或者設計。
1.樂觀鎖、悲觀鎖
樂觀鎖和悲觀鎖并不是兩種特定類型鎖,是人們定義的概念或者思想。主要是指人們看待同步的角度。
- 樂觀鎖:顧名思義就是樂觀的認為每次取數(shù)據(jù),別人都不會修改,所以不上鎖,但是在更新的時候會去判斷在此期間別人有沒有取更新這個數(shù)據(jù),可以使用版本號等機制,樂觀鎖適用于多讀的應用程序,這樣可以提高吞吐量,在java中原子變量類就是使用了樂觀鎖的一種實現(xiàn)方式CAS(compare and swap 比較并交換)來實現(xiàn)的
- 悲觀鎖:總是假設每次去獲取數(shù)據(jù),都認為別人會修改,所以每次拿取數(shù)據(jù)都會進行上鎖,這樣別人拿取數(shù)據(jù)就會阻塞,直到拿到鎖才行,比如Java里面的關鍵字synchronized實現(xiàn)就是悲觀鎖,悲觀鎖適合寫操作多的場景。
①. 樂觀鎖:樂觀鎖適合讀多的場景,不加鎖會代理大量的性能提升 ,在java編程中是無鎖編程,常常采用的是CAS算法。典型的例子就是原子類,通過CAS自旋實現(xiàn)原子的更新操作。
樂觀鎖更新判斷其他線程有沒有更新共享變量 一般采用數(shù)據(jù)版本機制或者CAS操作實現(xiàn)
(1):數(shù)據(jù)版本機制:一般兩種方式,一種是使用版本號,另一個是使用時間戳方式。
版本號方式:一般是在數(shù)據(jù)表上加上一個數(shù)據(jù)版本號version字段,表示更新的次數(shù),當數(shù)據(jù)被更新的時候計數(shù)加一,當線程A更新數(shù)據(jù)時候,會在讀取數(shù)據(jù)的同時也會讀取version字段,在更新提交的時候,若剛才的讀取的version和數(shù)據(jù)庫中的version相等才會更新,否則會重新進行更新操作。直到更新成功。
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
(2):CAS操作:當多個線程嘗試使用CAS同時更新一個變量時候,只有一個線程能夠更新變量,其他線程并不會被掛起,會收到通知失敗,并可以再次嘗試。
CAS需要三個字段值:1.需要讀寫的內(nèi)存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B),如果內(nèi)存位置的V值和預期原值A想匹配,那么就會更新B,否則不做變動。
②:悲觀鎖:悲觀鎖認為對于同一個數(shù)據(jù)的并發(fā)操作,一定會發(fā)生修改的,哪怕沒有修改,也會認為修改。因此對于同一份數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖并發(fā)操作一定會出問題。在對任意記錄進行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)。如果加鎖失敗,說明該記錄正在被修改,那么當前查詢可能要等待或者拋出異常。具體響應方式由開發(fā)者根據(jù)實際需要決定。如果成功加鎖,那么就可以對記錄做修改,事務完成后就會解鎖了。期間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。
2.獨享鎖、共享鎖
獨享鎖是指該鎖只能被一個線程獲取,共享鎖是指該鎖可以被多個線程持有。
對于java而言ReentrantLock是獨享鎖,但是對于另一個lock的實現(xiàn)類ReadWriterLock來說,其讀是共享鎖,其寫是獨占鎖。獨享鎖和共享鎖是通過AQS來實現(xiàn)的,通過不同的方法,來實現(xiàn)獨享或者共享(synchronized是獨占鎖)
AQS:AbstractQueueSynchronized抽象同步隊列,簡稱AQS;它是java并發(fā)包的基礎,并發(fā)的鎖就是基于Aqs實現(xiàn)的。
-
AQS是基于一個FIFO的雙向隊列,其內(nèi)部定義了一個node節(jié)點類,node節(jié)點內(nèi)部的SHARED用來標記該線程是獲取共享變量時被阻掛起后放入AQS隊列的,EXCLUSIVE用來標記線程是獨占資源時被掛起放入AQS隊列。
-
AQS使用一個volatile修飾的int類型的成員變量state來表示同步狀態(tài),修改同步狀態(tài)成功表示獲得鎖,volatile保證了變量在線程之間的可見性,修改state通過CAS機制來保證修改的原子性。
-
獲取state方式有兩種,獨占和共享。一個線程使用了獨占的方式,那么其他線程就失敗會被阻塞;一個線程使用共享時獲取資源,另一個線程還可以通過CAS的方式進行獲取。
-
如果共享資源被占用,需要一定的阻塞等待喚醒機制來保證鎖的分配,AQS會將獲取共享資源失敗的線程添加到一個變體的CLH中。
-
AQS中的ClH變體等待隊列特性
-
AQs中隊列是個雙鏈表,也是符合FIFO先進先出的特性。
-
通過head、tail兩個頭尾節(jié)點來組成隊列結(jié)構(gòu),通過volatile來保證可見性。
-
Head指向的節(jié)點本身已經(jīng)獲得了鎖,是一個虛擬節(jié)點,節(jié)點本身不具備具體線程
-
獲取不到同步狀態(tài),會將節(jié)點進行自旋獲取鎖,自旋一定次數(shù)失敗后,會將線程阻塞,相對于CLH隊列性能較好。
3.互斥鎖/讀寫鎖
- 講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現(xiàn)。
- 互斥鎖在Java中的具體實現(xiàn)就是ReentrantLock。
- 讀寫鎖在Java中的具體實現(xiàn)就是ReadWriteLock:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫。
4.可重入鎖:可重入鎖又名遞歸鎖,是指在同一個線程在外層獲鎖的時候,在進入內(nèi)存自動獲取鎖。也就是在執(zhí)行對象中所有的同步方法不用再次獲取鎖?!τ贘ava ReetrantLock而言,從名字就可以看出是一個重入鎖,其名字是Reentrant Lock 重新進入鎖。對于Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
synchronized void setA() throws Exception{Thread.sleep(1000);setB();
}synchronized void setB() throws Exception{Thread.sleep(1000);
}
上面的代碼就是一個可重入鎖的一個特點。如果不是可重入鎖的話,setB可能不會被當前線程執(zhí)行,可能造成死鎖。
4. 公平鎖和非公平鎖
- 公平鎖是指多個線程按照鎖的申請順序來獲取鎖,按等待時間來獲取鎖,等待時間長的線程有優(yōu)先獲取鎖的權利。
- 非公平鎖就是不是獲取鎖的順序不是按照申請鎖的順序,有可能后申請的先執(zhí)行,有可能會造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象。
- 對于Java ReetrantLock而言,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優(yōu)點在于吞吐量比公平鎖大。
- 對于Synchronized而言,也是一種非公平鎖。由于其并不像ReentrantLock是通過AQS的來實現(xiàn)線程調(diào)度,所以并沒有任何辦法使其變成公平鎖。
5.分段鎖
分段鎖是一種設計,并不是具體的一種鎖,對于ConcurrentHashMap而言是最好的例子,其并發(fā)就是通過分段式鎖來實現(xiàn)的
- ConcurrentHashMap實現(xiàn)原理:內(nèi)部分為了若干的小的hashmap,稱為段(segment),默認情況下一個ConcurrenthashMap
分為16段,即就是鎖的并發(fā)度,如果需要在ConcurrenthashMap中添加key-value,并不是將整個都加鎖,而是 首先根據(jù)hashcode計算出key-value應該存放在那個段中,然后對該段加鎖,并完成put操作,在多線程操作中,如果多個線程進行put操作,只要被加入的key-value不在同一個段中,則線程就可以實現(xiàn)真正的并行。 - 線程安全:ConcurrentHashMap 是一個 Segment 數(shù)組, Segment 通過繼承ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實現(xiàn)了全局的線程安全
-
6.偏向鎖/輕量鎖/重量級鎖
java每個對象都可以作為鎖,鎖有四種基本:無鎖、偏向鎖、輕量級鎖、重量級鎖,并且鎖可以進行升級不能下降,這三種是指鎖的狀態(tài),并且是針對synchronized的,是在java5,jdk1.6后引入實現(xiàn)高效升級synchronized,這三種鎖通過對象監(jiān)視器在對象頭中的字段表明。
- 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價。
在一段時間內(nèi),鎖不存在多線程競爭, 而是總是由同一個線程多次獲得,為了讓線程獲取鎖的代價更低就引入了偏向鎖的概念。怎么理解偏向鎖呢? 當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當前線程的 ID,后續(xù)這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。而是直接比較對象頭里面是否存儲了指向當前線程的偏向鎖。如果相等表示偏向鎖是偏向于當前線程的,就不需要再嘗試獲得鎖了。 - 輕量級鎖:是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
-重量級鎖:是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續(xù)下去,當自旋一定次數(shù)的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓他申請的線程進入阻塞,性能降低。
7.自旋鎖
自旋鎖是一種技術,是為了讓線程等待,我們只需要讓線程執(zhí)行一個忙循環(huán)。自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環(huán)的方式獲取鎖,這樣好處是減少線程上下文切換的消耗,缺點是循環(huán)會消耗cpu。
自旋鎖是一種非阻塞鎖,核心就是自旋兩個字,即用自旋代替阻塞操作,某一個線程嘗試獲取鎖的時候,如果該鎖已經(jīng)被另一個線程占用,那么這個這個線程將不斷循環(huán)進行檢查該鎖是否被釋放((默認次數(shù)是10,可以使用-XX:PreBlockSpinsh參數(shù)設置該值)),而不是讓此線程掛起或者睡眠,一旦另一個線程釋放鎖那么此線程就會立即獲得鎖。自旋是一種忙等待狀態(tài),過程會一直消耗cpu的時間片。
8.可中斷鎖
在等待鎖的過程中可以中斷。
9.死鎖
死鎖是一種現(xiàn)象,程A持有資源x,線程B持有資源y,線程A等待線程B釋放資源y,線程B等待線程A釋放資源x,兩個線程都不釋放自己持有的資源,則兩個線程都獲取不到對方的資源,就會造成死鎖。
死鎖不能自行打破,所以線程死鎖后,線程不能進行響應,所以要注意線程的使用并發(fā)場景。
死鎖形成條件
- 互斥條件:指線程對已經(jīng)獲取到的資源進行排他性使用。
- 請求并持有:指一個線程已經(jīng)持有了最少一個資源,但是有提出來新的資源請求,而新資源已經(jīng)被其他線程給占用,所以當前線程會被阻塞,但阻塞的同時不會釋放自己持有的資源。
- 不可剝奪條件:指線程獲取到的資源在自己使用完成之前不能被其它線程搶占,只能是自己在使用完畢后由自己進行釋放。
- 環(huán)路等待條件:指發(fā)生死鎖的時候,必然形成了一個線程------資源的環(huán)形鏈。
如何破壞避免形成死鎖呢:
- 條件1互斥條件肯定不能破壞,只能是下面三個條件進行破壞
- 請求并持有,我們可以一次性請求所有的數(shù)據(jù)。
- 對于不可剝奪條件,占用部分資源進一步申請其他資源的時候,如果申請不到,可以主動釋放它占有的資源,這樣不可搶占這個條件就失效了。
- 對于環(huán)路等待條件可以按順序進行申請資源來預防。
如何排查
可以使用jdk自帶的工具排查:
- 1.使用jps查找運行的java進程:jsp -1
- 2.使用jstack查看線程堆棧信息
- 3.可以利用圖形化工具Jconsole,出現(xiàn)死鎖點擊面板就能看見