JAVA篇最全梳理(1-3年經(jīng)驗(yàn)面經(jīng))(二)

2. JUC

總結(jié)不易,希望大家給點(diǎn)支持,堅(jiān)持每日一更,如果需要更加全面的資料可以聯(lián)系wx:qh950520

java 并發(fā)編程并發(fā):多個(gè)線程去訪問(wèn)同一個(gè)資源并行:各種事情同時(shí)去做,一邊干什么,一邊干什么

1. Synchronized

1.sync的使用

可以用用于代碼塊,普通方法,靜態(tài)方法

代碼塊----對(duì)象鎖,普通方法---對(duì)象鎖,靜態(tài)方法----類鎖

2.sync的主要作用

主要使用來(lái)解決多線程同步問(wèn)題的,其可以保證正在被修改的代碼任意時(shí)刻都只有一個(gè)線程執(zhí)行

3.sync的底層原理

被synchronized修飾的代碼,在被編譯器編譯后在被修飾的代碼前后加上了一組字節(jié)指令。

在代碼開始加入了monitorenter,在代碼后面加入了monitorexit,這兩個(gè)字節(jié)碼指令配合完成了synchronized關(guān)鍵字修飾代碼的互斥訪問(wèn)

在虛擬機(jī)執(zhí)行到monitorenter指令的時(shí)候,會(huì)請(qǐng)求獲取對(duì)象的monitor鎖,基于monitor鎖又衍生出一個(gè)鎖計(jì)數(shù)器的概念

當(dāng)執(zhí)行monitorenter時(shí),若對(duì)象未被鎖定時(shí),或者當(dāng)前線程已經(jīng)擁有了此對(duì)象的monitor鎖,則鎖計(jì)數(shù)器+1,該線程獲取該對(duì)象鎖。

當(dāng)執(zhí)行monitorexit時(shí),鎖計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器為0時(shí),此對(duì)象鎖就被釋放了。那么其他阻塞的線程則可以請(qǐng)求獲取該monitor鎖。

拓展:sync修飾普通方法底層原理:

普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來(lái)實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法再獲得同一個(gè)monitor對(duì)象。 其實(shí)本質(zhì)上沒(méi)有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成。

面試:Synchronized的可重入怎么實(shí)現(xiàn)的?

重入鎖實(shí)現(xiàn)可重入性原理或機(jī)制是:每一個(gè)鎖關(guān)聯(lián)一個(gè)線程持有者和計(jì)數(shù)器,當(dāng)計(jì)數(shù)器為 0 時(shí)表示該鎖沒(méi)有被任何線程持有,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法;當(dāng)某一線程請(qǐng)求成功后,JVM會(huì)記下鎖的持有線程,并且將計(jì)數(shù)器置為 1;此時(shí)其它線程請(qǐng)求該鎖,則必須等待;而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖,就可以再次拿到這個(gè)鎖,同時(shí)計(jì)數(shù)器會(huì)遞增;當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)器會(huì)遞減,如果計(jì)數(shù)器為 0,則釋放該鎖。

synchronized 方法若發(fā)生異常,則JVM會(huì)自動(dòng)釋放鎖。

鎖對(duì)象不能為空,否則拋出NPE(NullPointerException)

繼承創(chuàng)建了父類對(duì)象,并把父類對(duì)象的引用交給了子類,但是在super.去調(diào)用方法的時(shí)候JVM認(rèn)為調(diào)用者依然是子類。所以子類從寫父類sync方法,鎖對(duì)象都是子類。

2. volatile

volatile關(guān)鍵字

對(duì)volatile的理解:volatile是java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制,主要兩大特性:

保證可見性,禁止指令重排序但不保證原子性

為什么不保證原子性?

java只對(duì)基本數(shù)據(jù)類型變量的賦值和讀取是原子操作,想 i++這種是非原子操作,

1、讀內(nèi)存到寄存器;2、在寄存器中自增;3、寫回內(nèi)存。如果被volatile修飾了,肯定能保證每次讀取這個(gè)變量都是最新的值,但一旦需要對(duì)這個(gè)變量進(jìn)行自增這樣的非原子操作,就無(wú)法保證原子性了。

怎么保證可見性?

對(duì)volatile修飾的變量,執(zhí)行寫操作的話,JVM會(huì)發(fā)送一條lock前綴指令給CPU,CPU在計(jì)算完之后會(huì)立即將這個(gè)值寫回主內(nèi)存,同時(shí)因?yàn)橛?b>MESI緩存一致性協(xié)議,所以各個(gè)CPU都會(huì)對(duì)總線進(jìn)行嗅探,自己本地緩存中的數(shù)據(jù)是否被別人修改

如果發(fā)現(xiàn)別人修改了某個(gè)緩存的數(shù)據(jù),那么CPU就會(huì)將自己本地緩存的數(shù)據(jù)過(guò)期,然后這個(gè)CPU上執(zhí)行的線程在讀取那個(gè)變量的時(shí)候,就會(huì)從主內(nèi)存重新加載最新的數(shù)據(jù)。

lock前綴指令 + MESI緩存一致性協(xié)議

volatile為什么可以禁止指令重排序?

對(duì)于volatile修改變量的讀寫操作,都會(huì)加入內(nèi)存屏障

每個(gè)volatile寫操作前面,加StoreStore屏障,禁止上面的普通寫和他重排;每個(gè)volatile寫操作后面,加StoreLoad屏障,禁止跟下面的volatile讀/寫重排

每個(gè)volatile讀操作后面,加LoadLoad屏障,禁止下面的普通讀和voaltile讀重排;每個(gè)volatile讀操作后面,加LoadStore屏障,禁止下面的普通寫和volatile讀重排

sync怎么保證有序性?

synchronized則是由“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則獲取的。

3.Lock/ReenTrantLock/ReadWriteLock

synchronized和lock的區(qū)別?

sync是個(gè)java關(guān)鍵字,lock是個(gè)java類

sync無(wú)法判斷獲取鎖的狀態(tài),lock可以判斷是否獲取鎖

sync會(huì)自動(dòng)釋放鎖,lock需要手動(dòng)釋放,否則發(fā)生死鎖

線程獲取sync鎖后,另外一個(gè)線程會(huì)一直等待,而lock就不一定會(huì)等待(可中斷)

適合鎖少量同步代碼塊,Lock適合鎖大量同步代碼塊

java中有哪些鎖?

公平鎖/非公平鎖

公平鎖:多個(gè)線程按照順序獲取鎖

非公平鎖:線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序,有可能后申請(qǐng)的線程比先申請(qǐng)的線程優(yōu)先獲取鎖

可重入鎖

可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。 sync和reentrantlock

獨(dú)享鎖/共享鎖

獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有,ReentrantLock,sync,writeLock

共享鎖是指該鎖可被多個(gè)線程所持有,readLock

互斥鎖/讀寫鎖

互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock

讀寫鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock

樂(lè)觀鎖/悲觀鎖

樂(lè)觀鎖則認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,是不會(huì)發(fā)生修改的。在更新數(shù)據(jù)的時(shí)候,會(huì)采用嘗試更新,不斷重新的方式更新數(shù)據(jù)

悲觀鎖認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定是會(huì)發(fā)生修改的,哪怕沒(méi)有修改,也會(huì)認(rèn)為修改

樂(lè)觀鎖可以使用volatile+CAS原語(yǔ)實(shí)現(xiàn),帶參數(shù)版本來(lái)避免ABA問(wèn)題,在讀取和替換的時(shí)候進(jìn)行判定版本是否一致

悲觀鎖可以使用synchronize的以及Lock

分段鎖

分段鎖其實(shí)是一種鎖的設(shè)計(jì),并不是具體的一種鎖,對(duì)于ConcurrentHashMap而言,其并發(fā)的實(shí)現(xiàn)就是通過(guò)分段鎖的形式來(lái)實(shí)現(xiàn)高效的并發(fā)操作。

偏向鎖/輕量級(jí)鎖/重量級(jí)鎖

Synchronized

偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問(wèn),那么該線程會(huì)自動(dòng)獲取鎖。降低獲取鎖的代價(jià)。

輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,被另一個(gè)線程所訪問(wèn),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞,提高性能。

重量級(jí)鎖是指當(dāng)鎖為輕量級(jí)鎖的時(shí)候,另一個(gè)線程雖然是自旋,但自旋不會(huì)一直持續(xù)下去,當(dāng)自旋一定次數(shù)的時(shí)候,還沒(méi)有獲取到鎖,就會(huì)進(jìn)入阻塞,該鎖膨脹為重量級(jí)鎖重量級(jí)鎖會(huì)讓其他申請(qǐng)的線程進(jìn)入阻塞,性能降低。

自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會(huì)立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會(huì)消耗CPU。

原理:

第一個(gè)線程: 第一次來(lái)獲取鎖,立刻獲取到鎖

第二個(gè)線程: 線程一還沒(méi)有處理完,就要加入到隊(duì)列,并關(guān)心它的前驅(qū)節(jié)點(diǎn)鎖狀態(tài),然后進(jìn)行自旋。

第三步: 釋放鎖, 線程1把自身的locked = false ,同時(shí)把當(dāng)前節(jié)點(diǎn)改為了前節(jié)點(diǎn)。

死鎖是什么?

當(dāng)兩個(gè)線程循環(huán)依賴于一對(duì)同步對(duì)象(monitor)時(shí)將發(fā)生死鎖

比如線程1的占用了D資源,線程2的占用了C資源 ,此時(shí)線程1需要資源C,線程2需要D資源,他們就一直等待對(duì)方釋放資源,導(dǎo)致死鎖。

死鎖的4個(gè)必要條件?

當(dāng)且僅當(dāng)以下所有條件同時(shí)存在于系統(tǒng)中時(shí),才會(huì)出現(xiàn)資源上的死鎖情況:

相互排斥:系統(tǒng)中必須有非共享的資源,即在任何給定的時(shí)刻,只有一個(gè)進(jìn)程可以使用該資源。

保持等待:進(jìn)程當(dāng)前持有至少一個(gè)資源并請(qǐng)求其他進(jìn)程持有的其他資源。

不可搶占:資源只能由持有它的進(jìn)程自愿釋放,其他進(jìn)程不可強(qiáng)行占有該資源。

循環(huán)等待:每個(gè)進(jìn)程必須等待另一個(gè)進(jìn)程持有的資源,而該進(jìn)程又等待第一個(gè)進(jìn)程釋放資源

鎖優(yōu)化?

一般并發(fā)用到鎖就是阻塞的,因此鎖優(yōu)化就是在堵塞的情況下去提高性能

? 持有時(shí)間減少和鎖的范圍 (比如以前是對(duì)整個(gè)方法加鎖,現(xiàn)在只對(duì)方法內(nèi)代碼塊加鎖)

? 減小鎖粒度(高性能的hash表,他就是做了減少鎖粒度的實(shí)現(xiàn),他被拆分好像16個(gè)Segment,每個(gè)Segment就是一個(gè)個(gè)小的hashmap.。就是把大的hash表拆成若干個(gè)小的hash表。)在減小鎖粒度后, ConcurrentHashMap允許若干個(gè)線程同時(shí)進(jìn)入

? 鎖分離(分離,就是讀寫鎖分離,讀不用改變數(shù)據(jù),所以所有的讀不會(huì)產(chǎn)生堵塞,讀不會(huì)產(chǎn)生堵塞。當(dāng)寫的時(shí)候才去進(jìn)行堵塞,對(duì)于讀多寫少的場(chǎng)景,應(yīng)用比較好)

? 鎖粗化(對(duì)同一個(gè)鎖不停的進(jìn)行請(qǐng)求、同步和釋放,其本身也會(huì)消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化。因此可以把很多次請(qǐng)求的鎖拿到一個(gè)鎖里面,但前提是:中間不需要的同步的代碼塊很很快的執(zhí)行完。

例如:for(in i=0;i<count;i++){

Synchronized()

}

改為

Synchronized(){

for(in i=0;i<count;i++){}

}

? 鎖消除

在即時(shí)編譯器時(shí),如果發(fā)現(xiàn)不可能被共享的對(duì)象,則可以消除這些對(duì)象的鎖操作。

比如StringBuffer ,他本來(lái)就是線程安全的,他內(nèi)部有很多sycn修飾

ConCurrentHashMap分段鎖的概念

ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap(JDK7與JDK8中HashMap的實(shí)現(xiàn))的結(jié)構(gòu),即內(nèi)部擁有一個(gè)Entry數(shù)組,數(shù)組中的每個(gè)元素又是一個(gè)鏈表;同時(shí)又是一個(gè)ReentrantLock(Segment繼承了ReentrantLock)。

當(dāng)需要put元素的時(shí)候,并不是對(duì)整個(gè)hashmap進(jìn)行加鎖,而是先通過(guò)hashcode來(lái)知道他要放在那一個(gè)分段中,然后對(duì)這個(gè)分段進(jìn)行加鎖,所以當(dāng)多線程put的時(shí)候,只要不是放在一個(gè)分段中,就實(shí)現(xiàn)了真正的并行的插入。

但是,在統(tǒng)計(jì)size的時(shí)候,可就是獲取hashmap全局信息的時(shí)候,就需要獲取所有的分段鎖才能統(tǒng)計(jì)。

分段鎖的設(shè)計(jì)目的是細(xì)化鎖的粒度,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候,就僅僅針對(duì)數(shù)組中的一項(xiàng)進(jìn)行加鎖操作。

我們面對(duì)ReentrantLock和synchronized改如何選擇?

Synchronized相比Lock,為許多開發(fā)人員所熟悉,并且簡(jiǎn)潔緊湊,如果現(xiàn)有程序已經(jīng)使用了內(nèi)置鎖,那么盡量保持代碼風(fēng)格統(tǒng)一,盡量不引入Lock,避免兩種機(jī)制混用,容易令人困惑,也容易發(fā)生錯(cuò)誤。在Synchronized無(wú)法滿足需求的情況下,Lock可以作為一種高級(jí)工具,這些功能包括“可定時(shí)的、可輪詢的可中斷的鎖獲取操作,公平隊(duì)列,以及非塊結(jié)構(gòu)的鎖否則還是優(yōu)先使用Synchronized。最后,未來(lái)更可能提升Synchronized而不是Lock的性能,因?yàn)?b>Synchronized是JVM的內(nèi)置屬性,他能執(zhí)行一些優(yōu)化,例如對(duì)線程封閉的鎖對(duì)象的鎖消除優(yōu)化,通過(guò)增加鎖的粒度來(lái)消除內(nèi)置鎖的同步,而如果基于類庫(kù)的鎖來(lái)實(shí)現(xiàn)這些功能,則可能性不大

4.CAS

原理:比較當(dāng)前工作內(nèi)存中的值和主內(nèi)存中的值,如果這個(gè)值是期望的,那么則執(zhí)行操作。如果不是就一直循環(huán)。

缺點(diǎn):

底層是自旋鎖,耗時(shí)

一次性只能保存一個(gè)共享變量的原子性

ABA問(wèn)題

ABA問(wèn)題:需要取出內(nèi)存中某時(shí)刻的數(shù)據(jù),而在下時(shí)刻比較并替換,那么在這個(gè)時(shí)間差類會(huì)導(dǎo)致數(shù)據(jù)的變化。

典型例子: 現(xiàn)有一個(gè)用單向鏈表實(shí)現(xiàn)的棧,棧頂為A,這時(shí)線程T1已經(jīng)知道A.next為B,然后希望用CAS將棧頂替換為B:head.compareAndSet(A,B);在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A。此時(shí)輪到線程T1執(zhí)行CAS操作,檢測(cè)發(fā)現(xiàn)棧頂仍為A,所以CAS成功,棧頂變?yōu)锽,但實(shí)際上B.next為null,這樣CD就丟失了。

樂(lè)觀鎖的實(shí)現(xiàn)中通常都會(huì)用版本戳version來(lái)對(duì)記錄或?qū)ο髽?biāo)記,避免并發(fā)操作帶來(lái)的問(wèn)題,在Java中,AtomicStampedReference<E>也實(shí)現(xiàn)了這個(gè)作用

AtomicStampedReference(intialRef, initialStamp)

比較initalStamp,然后進(jìn)行compareAndSet

Unsafe 類

//java無(wú)法直接操作內(nèi)存,java可以調(diào)用C++,native方法,C++可以操作內(nèi)存

java通過(guò)Unsafe類可以操作native方法,來(lái)操作內(nèi)存

5.并發(fā)集合類

并發(fā)包下list集合CopyOnWriteArrayList

1.Vector? 已經(jīng)廢棄了,效率太低

2.List<String> list = Collections.synchronizedList(new ArrayList<>());? //效率低

3.List<String> list = new CopyOnWriteArrayList<>(new ArrayList<>());? JUC里面的方法,add加lock鎖CopyOnWrite 寫入時(shí)復(fù)制 COW 計(jì)算機(jī)領(lǐng)域的一種策略:多個(gè)線程調(diào)用的時(shí)候,List讀取的時(shí)候固定不變,寫入的時(shí)候存在覆蓋問(wèn)題。

在寫入的時(shí)候避免覆蓋創(chuàng)造的數(shù)據(jù)問(wèn)題,復(fù)制一份給調(diào)用者,調(diào)用完畢返回。

并發(fā)包下set集合CopyOnWriteArraySet

1.List<String> list = Collections.synchronizedSet(new ArraySet<>());? //效率低

2.List<String> list = new CopyOnWriteArraySet<>(new ArraySet<>());? JUC里面的方法,add加lock鎖CopyOnWrite 寫入時(shí)復(fù)制 COW 計(jì)算機(jī)領(lǐng)域的一種策略:

Queue

拋出異常是:NoSuchElementException


6.并發(fā)的三個(gè)輔助類

CountDownLatch 減法計(jì)數(shù)器,當(dāng)計(jì)數(shù)器為0時(shí)才向下執(zhí)行,否則await一直阻塞


CycliBarrier 加法計(jì)數(shù)器,達(dá)到指定記數(shù)值就會(huì)觸發(fā)下面流程

public class CyclicBarrierTest{

publicstaticvoidmain(String[]args)throwsBrokenBarrierException,InterruptedException{

/*

CyclicBarrier 構(gòu)造器有兩個(gè)參數(shù)

1. int? 要計(jì)數(shù)到的值

2. Runnable 達(dá)到計(jì)數(shù)器之后要執(zhí)行的線程

*/

CyclicBarriercyclicBarrier=newCyclicBarrier(7,()->{

System.out.println("召喚神龍成功");

? ? ?? });

?

for(inti=1;i<=7;i++) {

finalinttemp=i;

// Lambda 能操作 i 嗎

newThread(()->{

System.out.println(Thread.currentThread().getName()+"收集了:"+temp+"顆龍珠");

try{

cyclicBarrier.await();

}catch(InterruptedExceptione) {

e.printStackTrace();

}catch(BrokenBarrierExceptione) {

e.printStackTrace();

? ? ? ? ? ? ?? }

},String.valueOf(i)).start();

? ? ?? }

?? }

}

Semaphore sqmaphore.acquire() 假如線程已經(jīng)滿了,就等待有空為止.semaphore.release,


7.讀寫鎖


8.JMM

9.AQS

AbstractQueuedSynchronizer抽象隊(duì)列同步器簡(jiǎn)稱AQS

ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,F(xiàn)utureTask等等皆是基于AQS的。

ReentrantLcok 底層加鎖原理

ReentrantLcok 內(nèi)有個(gè)AQS對(duì)象,這個(gè)AQS中含有一個(gè)核心變量state,是int類型的,代表加鎖狀態(tài),初始狀態(tài)是0,另外AQS還有一個(gè)關(guān)鍵變量,用來(lái)標(biāo)記當(dāng)前加鎖的是哪個(gè)線程,初始為null。

當(dāng)一個(gè)線程調(diào)用lock(), 直接利用CAS將state從0變?yōu)?,1表示加鎖成功,然后再設(shè)置當(dāng)前加鎖線程就是自己。

ReenTrantLock 互斥實(shí)現(xiàn)原理:

線程2跑過(guò)來(lái)一下看到,state的值不是0啊?所以CAS操作將state從0變?yōu)?的過(guò)程會(huì)失敗,因?yàn)閟tate的值當(dāng)前為1,說(shuō)明已經(jīng)有人加鎖了!

線程2會(huì)將自己放入AQS中的一個(gè)等待隊(duì)列,因?yàn)樽约簢L試加鎖失敗了,此時(shí)就要將自己放入隊(duì)列中來(lái)等待,等待線程1釋放鎖之后,自己就可以重新嘗試加鎖了

AQS就是一個(gè)并發(fā)包的基礎(chǔ)組件,用來(lái)實(shí)現(xiàn)各種鎖,各種同步組件的。

它包含了state變量、加鎖線程等待隊(duì)列等并發(fā)中的核心組件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容