2 萬字長文詳解 10 大多執行緒面試題|原力計劃

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

作者 | ZZZhonngger

責編 | 伍杏玲

出品 | CSDN部落格

Volatile相關

1.請談談你對 volatile 的理解

答:volatile 是 Java 虛擬機器提供的輕量級的同步機制。

保證可見性

不能保證原子性

禁止指令重排序

要完整地回答好這題,還需要理解Java記憶體模型(JMM)。

JMM 本身是一種抽象的概念並不是真實存在,它描述的是一組規定或則規範,透過這組規範定義了程式中的訪問方式。

JMM 同步規定:

執行緒解鎖前,必須把共享變數的值重新整理回主記憶體

執行緒加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體

加鎖解鎖是同一把鎖

速率:CPU>記憶體>硬碟,CPU為了保證高效地工作,會將資料沖刷到緩衝區中。

由於 JVM 執行程式的實體是執行緒,而每個執行緒建立時 JVM 都會為其建立一個工作記憶體,工作記憶體是每個執行緒的私有資料區域,而 Java 記憶體模型中規定所有變數的儲存在主記憶體,主記憶體是共享記憶體區域,所有的執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須都工作記憶體進行看。

首先要將變數從主記憶體複製的自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本複製,前面說過,工作記憶體是每個執行緒的私有資料區域,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須透過主記憶體來完成。

Java記憶體模型圖:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

JMM模型中的一個重要概念:可見性,即某個執行緒修改了某個共享變數的值,並把該共享變數寫回主記憶體中,其他執行緒要知道該變數被修改過了。

(1)驗證volatile的可見性

package Day34;

/**

* @Author Zhongger

* @Description 驗證Volatile的可見性

* @Date 2020。3。4

*/

publicclassVolatileDemo {

publicstaticvoidmain(String[] args) {

MyData myData = new MyData();

new Thread(()->{

System。out。println(Thread。currentThread()。getName()+“\t come in”);

try { Thread。sleep(3000); } catch (InterruptedException e) { e。printStackTrace(); }

myData。addOne();

System。out。println(Thread。currentThread()。getName()+“\t updated a=”+myData。a);

},“A”)。start();

//第二個執行緒為main執行緒

while (myData。a==0){

//如果執行緒間的可見性不能保證,那麼此迴圈回成為死迴圈

}

//如果執行到以下語句,證明volatile可以保證執行緒間的可見性

System。out。println(Thread。currentThread()。getName()+“\t come here”);

}

}

classMyData {

//int a = 0;

volatileint a = 0;

voidaddOne() {

this。a += 1;

}

}

如果不加 volatile 關鍵字,則主執行緒會進入死迴圈,加 volatile 則主執行緒能夠退出,說明加了 volatile 關鍵字變數,當有一個執行緒修改了值,會馬上被另一個執行緒感知到,當前值作廢,從新從主記憶體中獲取值。對其他執行緒可見,這就叫可見性。

(2)測試volatile不能保證原子性

publicclassVolatileDemo {

publicstaticvoidmain(String[] args) {

test2();

}

publicstaticvoidtest2(){

MyData data = new MyData();

for (int i = 0; i < 20; i++) {

new Thread(() -> {

for (int j = 0; j < 1000; j++) {

data。addOne();

}

})。start();

}

// 預設有 main 執行緒和 gc 執行緒

while (Thread。activeCount() > 2) {

Thread。yield();

}

System。out。println(data。a);

}

}

classMyData {

//int a = 0;

volatileint a = 0;

voidaddOne() {

this。a += 1;

}

}

發現並不能輸出 20000,因此這就沒有保證原子性了。另外要注意,number++在多執行緒的情況下是執行緒不安全的,雖然可以使用synchronized給方法加鎖,但最好不要殺雞用牛刀,稍後會將解決方案。那麼為什麼volatile不能保證原子性呢?主要是因為寫值丟失的情況。來看一下下面的程式碼:

classTest{

volatileint n=0;

publicvoidadd(){

n++;

}

}

編譯成位元組碼檔案是這樣的:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

要執行n++,需要進行三步,一是獲得n的值,然後+1,然後寫回主記憶體。如果沒有加synchronized,所有的執行緒都有可能掙搶到n,並把n複製到自己的工作記憶體區,然後執行加1操作,然後多個執行緒把操作完的n寫入回主記憶體中,這就容易導致寫覆蓋,即執行緒排程時某個寫的執行緒被掛起了,等到它被喚醒之後又把這個值寫進去,而沒有對新的值進行修改。

那麼怎麼解決呢?使用JUC下的AtomicInteger

AtomicInteger atomicInteger=new AtomicInteger();//預設為0

publicvoidaddAtomic(){

atomicInteger。incrementAndGet();//相當於n++

}

JMM規範中要求保證有序性,看看以下的解釋:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

volatile 實現禁止指令重排序的最佳化,從而避免了多執行緒環境下程式出現亂序的現象。

先了解一個概念,記憶體屏障(Memory Barrier)又稱記憶體柵欄,是一個 CPU 指令,作用有兩個:

保證特定操作的執行順序

保證某些變數的記憶體可見性(利用該特性實現 volatile 的記憶體可見性)

由於編譯器個處理器都能執行指令重排序最佳化,如果在指令間插入一條 Memory Barrier 則會告訴編譯器和 CPU,不管什麼指令都不能個這條 Memory Barrier 指令重排序,也就是說透過插入記憶體屏障禁止在記憶體屏障前後執行重排序最佳化。記憶體屏障另一個作用是強制刷出各種 CPU 快取資料,因此任何 CPU 上的執行緒都能讀取到這些資料的最新版本。

下面是保守策略下,volatile寫插入記憶體屏障後生成的指令序列示意圖:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

下面是在保守策略下,volatile讀插入記憶體屏障後生成的指令序列示意圖:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

執行緒安全性保證:

工作記憶體與主記憶體同步延遲現象導致可見性問題:可以使用 synchronzied 或 volatile 關鍵字解決,它們可以使用一個執行緒修改後的變數立即對其他執行緒可見

對於指令重排導致可見性問題和有序性問題,可以利用 volatile 關鍵字解決,因為 volatile 的另一個作用就是禁止指令重排序最佳化

2、你在哪些地方用到過 volatile?

單例模式

(DCL,double check lock雙端檢鎖機制)

//volatile是輕量級Synchronized,保證記憶體的可見性,防止指令重排序

classSingleTon{

privatestaticvolatile SingleTon singleTon;

privateSingleTon(){

}

//解決執行緒安全問題,同時解決懶載入問題,也保證了效率

publicstaticsynchronized SingleTon getSingleTon(){

if (singleTon == null) {

//同步程式碼效率較低

synchronized (SingleTon。class) {

if (singleTon == null) {

singleTon = new SingleTon();

}

}

}

return singleTon;

}

}

DCL機制確不一定執行緒安全,原因是有指令重排序的存在,所以需要加入 volatile 可以禁止指令重排。當某一個執行緒執行到第一次檢測,讀取到的 instance 不為 null 時,instance 的引用物件可能還沒有完成初始化。指令重排只會保證單執行緒情況下語義執行的一致性,而不會保證多執行緒的情況。

instance = new Singleton()

可以分為以下三步完成(虛擬碼):

memory = allocate(); // 1。分配物件空間

instance(memory); // 2。初始化物件

instance = memory; // 3。設定instance指向剛分配的記憶體地址,此時instance != null

步驟 2 和步驟 3 不存在依賴關係,而且無論重排前還是重排後程序的執行結果在單執行緒中並沒有改變,因此這種最佳化是允許的。指令可能會重排為以下情況:

memory = allocate(); // 1。分配物件空間

instance = memory; // 3。設定instance指向剛分配的記憶體地址,此時instance != null,但物件還沒有初始化完成

instance(memory); // 2。初始化物件

所以不加 volatile 返回的例項不為空,但可能是未初始化的例項。

CAS相關

1。先看一個小Demo

package Day35;

import java。util。concurrent。atomic。AtomicInteger;

/**

* @Author Zhongger

* @Description CAS演算法——比較和交換

* @Date 2020。3。5

*/

publicclassCASDemo{

publicstaticvoidmain(String[] args){

AtomicInteger atomicInteger = new AtomicInteger(5);

//三個執行緒獲取主記憶體中的值,並當主記憶體中的值為5時,替換為2020

for (int i = 0; i < 3; i++) {

System。out。println(atomicInteger。compareAndSet(5, 2020)+“\t current data:”+atomicInteger。get());

}

}

}

執行結果:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

下面用這幅圖來輔助理解上述程式碼:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

首先,主記憶體中的值為5,當前有三個執行緒,它們將5複製到自己的工作記憶體中,進行下一步的操作;然後,假設Thread1搶佔到了資源,呼叫了atomicInteger。compareAndSet(5, 2020)方法,該方法的作用是,如果期望值是5的話,那麼就使用2020去替換掉它,顯然這個時候atomicInteger=5,可以把5替換成2020,然後把2020沖刷回主記憶體中,並通知其他執行緒可見,返回true;當Thread2和Thread3想要呼叫atomicInteger。compareAndSet(5, 2020)方法時,發現期望值已經不是5而是2020了,所以就無法再用2020進行替換了,返回false。

2.底層原理

自旋鎖和Unsafe類

先來看看Unsafe類,AtomicInteger類中的getAndIncrement()方法:

/**

* Atomically increments by one the current value。

*

* @return the previous value

*/

public final intgetAndIncrement() {

returnunsafe。getAndAddInt(this, valueOffset, 1);

}

發現呼叫了unsafe類的方法。

這是AtomicInteger類的部分原始碼:

publicclassAtomicIntegerextendsNumberimplementsjava。io。Serializable{

privatestaticfinallong serialVersionUID = 6214790243416807050L;

// setup to use Unsafe。compareAndSwapInt for updates

privatestaticfinal Unsafe unsafe = Unsafe。getUnsafe();

privatestaticfinallong valueOffset;

static {

try {

// 獲取下面 value 的地址偏移量

valueOffset = unsafe。objectFieldOffset

(AtomicInteger。class。getDeclaredField(“value”));

} catch (Exception ex) { thrownew Error(ex); }

}

privatevolatileint value;

// ……

}

獲取Unsafe類,此類是rt。jar包下的類:

privatestatic final Unsafe unsafe = Unsafe。getUnsafe();

Unsafe 是 CAS 的核心類,由於 Java 方法無法直接訪問底層系統,而需要透過本地(native)方法來訪問, Unsafe 類相當一個後門,基於該類可以直接操作特定記憶體的資料。Unsafe 類存在於 sun。misc 包中,其內部方法操作可以像 C 指標一樣直接操作記憶體,因為 Java 中 CAS 操作執行依賴於 Unsafe 類。

變數 vauleOffset,表示該變數值在記憶體中的偏移量,因為 Unsafe 就是根據記憶體偏移量來獲取資料的。

變數 value 用 volatile 修飾,保證了多執行緒之間的記憶體可見性。

3.CAS是什麼

CAS 的全稱 Compare-And-Swap,它是一條 CPU 併發原語。

它的功能是判斷記憶體某一個位置的值是否為預期,如果是則更改這個值,這個過程就是原子的。

CAS 併發原語體現在 Java中就是 sun。misc。Unsafe 類中的各個方法。呼叫 UnSafe 類中的 CAS 方法,JVM 會幫我們實現出 CAS 彙編指令。這是一種完全依賴硬體的功能,透過它實現了原子操作。由於 CAS 是一種系統原語,原語屬於作業系統用語範疇,是由若干條指令組成,用於完成某一個功能的過程,並且原語的執行必須是連續的,在執行的過程中不允許被中斷,也就是說 CAS 是一條原子指令,不會造成所謂的資料不一致的問題。

分析一下 unsafe。getAndAddInt()方法來更好地理解CAS和自旋鎖:

obj為AtomicInteger物件本身

valueOffset為該物件的引用地址

expected為期望修改的值

val為修改的數值

publicfinalintgetAndAddInt(Object obj, long valueOffset, long expected, int val){

int temp;

do {

temp = this。getIntVolatile(obj, valueOffset); // 獲取當前物件在其地址上的快照值

} while (!this。compareAndSwap(obj, valueOffset, temp, temp + val)); // 如果此時 temp 沒有被修改,把其值修改成temp+val,就能退出迴圈;否則重新獲取,這個迴圈的過程,就相當於自旋鎖。

return temp;

}

4.CAS 的缺點

迴圈時間長開銷很大。如果 CAS 失敗,會一直嘗試,如果 CAS 長時間一直不成功,可能會給 CPU 帶來很大的開銷(比如執行緒數很多,每次比較都是失敗,就會一直迴圈),所以希望是執行緒數比較小的場景。

只能保證一個共享變數的原子操作。對於多個共享變數操作時,迴圈 CAS 就無法保證操作的原子性。

引出 ABA 問題。

ABA問題相關

1.ABA問題

CAS演算法實現一個重要前提是需要提取記憶體中某時刻的資料並在當下時刻比較並替換,那麼在這個時間差內會導致資料的變化。比如說一個執行緒1從記憶體位置V中取出A,這時候另一個執行緒2也從記憶體位置中取出A,並且執行緒2進行了一些操作將值變成了B,然後執行緒2有將值變成了A,這時候執行緒1進行CAS操作時發現記憶體中仍然是A,然後執行緒1操作成功。儘管執行緒1的CAS操作成功,但不代表這個過程就是沒有問題的。

2.演示一下原子引用

package Day37;

import java。util。concurrent。atomic。AtomicReference;

/**

* @Author Zhongger

* @Description 原子引用演示

* @Date 2020。3。7

*/

publicclassAtomicReferenceDemo {

publicstaticvoidmain(String[] args) {

AtomicReference userAtomicReference = new AtomicReference<>();

User user1 = new User(“A”,21);

User user2 = new User(“B”, 26);

userAtomicReference。set(user1);//將主物理記憶體中的值設定為A

System。out。println(userAtomicReference。compareAndSet(user1,user2)+“\t”+userAtomicReference。get()。toString());//CAS演算法

System。out。println(userAtomicReference。compareAndSet(user1,user2)+“\t”+userAtomicReference。get()。toString());//經過上一行程式碼的CAS演算法,主物理記憶體中的值是B而不是A,返回false

}

}

classUser{

private String username;

privateint age;

publicUser(String username, int age) {

this。username = username;

this。age = age;

}

public String getUsername() {

return username;

}

publicvoidsetUsername(String username) {

this。username = username;

}

publicintgetAge() {

return age;

}

publicvoidsetAge(int age) {

this。age = age;

}

@Override

public String toString() {

return“User{” +

“username=‘” + username + ’\‘’ +

“, age=” + age +

‘}’;

}

}

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

3.如何解決ABA問題?

時間戳原子引用。一種具備版本號機制的原子引用類,每修改一個值時,就將版本號更新。

先看一下產生ABA問題的程式碼:

package Day37;

import java。util。concurrent。TimeUnit;

import java。util。concurrent。atomic。AtomicReference;

/**

* @Author Zhongger

* @Description ABA問題

* @Date 2020。3。7

*/

publicclassABAProblem {

privatestatic AtomicReference atomicReference=new AtomicReference<>(100);

publicstaticvoidmain(String[] args) {

new Thread(()->{

atomicReference。compareAndSet(100,127);//由於Integer的範圍,expect和update的值都應該在-128~127之間

System。out。println(“100->101:”+atomicReference。get());

atomicReference。compareAndSet(127,100);//ABA操作,將100改成127,然後將127又改回100

System。out。println(“101->100:”+atomicReference。get());

},“Thread1”)。start();

new Thread(()->{

try {//確保Thread1完成一次ABA操作

TimeUnit。SECONDS。sleep(1);

} catch (InterruptedException e) {

e。printStackTrace();

}

atomicReference。compareAndSet(100,2020);

//讀取到主存中值仍然為100,執行更新操作,其實中途主存的值發生了100->127->100的變化

System。out。println(“最終結果”+atomicReference。get());//返回2020

},“Thread2”)。start();

}

}

結果為:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

引入AtomicStampedReference類來解決ABA問題,使得版本號不一致的CAS操作無法完成。

package Day37;

import java。util。concurrent。atomic。AtomicStampedReference;

/**

* @Author Zhongger

* @Description 使用AtomicStampedReference來解決ABA問題

* @Date 2020。3。7

*/

publicclassABASolution {

privatestatic AtomicStampedReference atomicStampedReference=new AtomicStampedReference<>(100,1);

//主記憶體中初始值為100,版本號為1

publicstaticvoidmain(String[] args) {

new Thread(()->{

int stamp = atomicStampedReference。getStamp();//當前版本號

System。out。println(Thread。currentThread()。getName()+“\t 第1次的版本號”+stamp);

try { Thread。sleep(2000); } catch (InterruptedException e) { e。printStackTrace(); }//等待Thread2也拿到相同的版本號

atomicStampedReference。compareAndSet(100,127,atomicStampedReference。getStamp(),atomicStampedReference。getStamp()+1);//更新一次,版本號加1

System。out。println(Thread。currentThread()。getName()+“\t 第2次的版本號”+atomicStampedReference。getStamp());

atomicStampedReference。compareAndSet(127,100,atomicStampedReference。getStamp(),atomicStampedReference。getStamp()+1);//更新一次,版本號加1

System。out。println(Thread。currentThread()。getName()+“\t 第3次的版本號”+atomicStampedReference。getStamp());

},“Thread1”)。start();

new Thread(()->{

int stamp = atomicStampedReference。getStamp();//當前版本號

System。out。println(Thread。currentThread()。getName()+“\t 第1次的版本號”+stamp);

try { Thread。sleep(4000); } catch (InterruptedException e) { e。printStackTrace(); }//等待Thread1完成一次ABA操作

boolean result = atomicStampedReference。compareAndSet(100, 2020, stamp, stamp + 1);

int newStamp = atomicStampedReference。getStamp();//最新版本號

System。out。println(Thread。currentThread()。getName()+“\t修改成功否”+result+“當前實際版本號”+newStamp);

System。out。println(Thread。currentThread()。getName()+“\t當前最新值”+atomicStampedReference。getReference());

},“Thread2”)。start();

}

}

執行結果如下:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

可見,當Thread2與Thread1的版本號不一致時,CAS操作無法完成。

集合類執行緒

集合類執行緒不安全,請編寫一個不安全的案例並給出解決方案。

publicclassArrayListDemo {

publicstaticvoidmain(String[] args){

List list = new ArrayList<>();

Random random = new Random();

for (int i = 0; i < 100; i++) {

new Thread(() -> {

list。add(random。nextInt(10));

System。out。println(list);

})。start();

}

}

}

發現報 java。util。ConcurrentModificationException 併發修改異常。

解決方案

new Vector();

Collections。synchronizedList(new ArrayList<>());

new CopyOnWriteArrayList<>();

最佳化建議

在讀多寫少的時候推薦使用 CopeOnWriteArrayList 這個類。另外,Set和Map的案例這裡就不寫了,可以看我之前寫過的一篇部落格。

https://blog。csdn。net/weixin_43395911/article/details/104586689

Java中的鎖

1.公平和非公平鎖

公平鎖:是指多個執行緒按照申請的順序來獲取值。

非公平鎖:是指多個執行緒獲取值的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖,在高併發的情況下,可能會造成優先順序翻轉或者飢餓現象。

兩者區別:

公平鎖:在併發環境中,每一個執行緒在獲取鎖時會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒是等待佇列的第一個就佔有鎖,否者就會加入到等待佇列中,以後會按照 FIFO 的規則獲取鎖。

非公平鎖:一上來就嘗試佔有鎖,如果失敗再進行排隊。在JUC中ReentrantLock的建立可以知道建構函式的boolean型別來獲得公平鎖或非公平鎖,預設是非公平鎖;synchronized也是一種非公平鎖。非公平鎖的吞吐量比公平鎖的大。

2.可重入鎖和不可重入鎖

可重入鎖

:也叫遞迴鎖,指的是同一個執行緒外層函式獲得鎖之後,內層遞迴函式仍然能獲取到該鎖,在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法或會自動獲取該鎖。可重入鎖的最大作用是避免死鎖。

不可重入鎖

:所謂不可重入鎖,即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。

package Day38;

/**

* @Author Zhongger

* @Description 可重入鎖演示

* @Date 2020。3。8

*/

publicclassReentrantLockDemo{

publicstaticvoidmain(String[] args){

Phone phone = new Phone();

new Thread(()->{

phone。sendSMS();

},“A”)。start();

new Thread(()->{

phone。sendSMS();

},“B”)。start();

}

}

//資源類

classPhone{

publicsynchronizedvoidsendSMS(){

System。out。println(Thread。currentThread()。getName() + “\t 執行了sendSMS方法”);

sendEmail();

//sendSMS()方法呼叫了加了鎖的sendEmail方法,如果Thread。currentThread()。getName()是一致的

//說明synchronized是可重入鎖

}

publicsynchronizedvoidsendEmail(){

System。out。println(Thread。currentThread()。getName() + “\t 執行了sendEmail方法”);

}

}

執行結果如下:

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

由此可見,當一個執行緒拿到了synchronized鎖時,可以執行所有的帶有synchronized的方法,當然普通方法也是可以的。

同理,ReentrantLock同樣也是可重入鎖。

手寫一個可重入鎖:

publicclassReentrantLock{

boolean isLocked = false;

Thread lockedBy = null;

int lockedCount = 0;

publicsynchronizedvoidlock()throws InterruptedException {

Thread thread = Thread。currentThread();

while (isLocked && lockedBy != thread) {

wait();

}

isLocked = true;

lockedCount++;

lockedBy = thread;

}

publicsynchronizedvoidunlock(){

if (Thread。currentThread() == lockedBy) {

lockedCount——;

if (lockedCount == 0) {

isLocked = false;

notify();

}

}

}

}

測試:

publicclassCount {

ReentrantLock lock = new ReentrantLock();

publicvoidprint() throws InterruptedException{

lock。lock();

doAdd();

lock。unlock();

}

privatevoiddoAdd() throws InterruptedException {

lock。lock();

// do something

System。out。println(“ReentrantLock”);

lock。unlock();

}

publicstaticvoidmain(String[] args) throws InterruptedException {

Count count = new Count();

count。print();

}

}

發現可以輸出 ReentrantLock,我們設計兩個執行緒呼叫 print() 方法,第一個執行緒呼叫 print() 方法獲取鎖,進入 lock() 方法,由於初始 lockedBy 是 null,所以不會進入 while 而掛起當前執行緒,而是是增量 lockedCount 並記錄 lockBy 為第一個執行緒。接著第一個執行緒進入 doAdd() 方法,由於同一程序,所以不會進入 while 而掛起,接著增量 lockedCount,當第二個執行緒嘗試lock,由於 isLocked=true,所以他不會獲取該鎖,直到第一個執行緒呼叫兩次 unlock() 將 lockCount 遞減為0,才將標記為 isLocked 設定為 false。

手寫一個不可重入鎖:

publicclassNotReentrantLock{

privateboolean isLocked = false;

publicsynchronizedvoidlock()throws InterruptedException {

while (isLocked) {

wait();

}

isLocked = true;

}

publicsynchronizedvoidunlock(){

isLocked = false;

notify();

}

}

測試:

publicclassCount {

NotReentrantLock lock = new NotReentrantLock();

publicvoidprint() throws InterruptedException{

lock。lock();

doAdd();

lock。unlock();

}

privatevoiddoAdd() throws InterruptedException {

lock。lock();

// do something

lock。unlock();

}

publicstaticvoidmain(String[] args) throws InterruptedException {

Count count = new Count();

count。print();

}

}

當前執行緒執行print()方法首先獲取lock,接下來執行doAdd()方法就無法執行doAdd()中的邏輯,必須先釋放鎖。這個例子很好的說明了不可重入鎖。

3.自旋鎖

是指定嘗試獲取鎖的執行緒不會立即堵塞,而是採用迴圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒上線文切換的消耗,缺點就是迴圈會消耗 CPU。

手動實現自旋鎖

publicclassSpinLock {

private AtomicReference atomicReference = new AtomicReference<>();

privatevoidlock () {

System。out。println(Thread。currentThread() + “ coming。。。”);

while (!atomicReference。compareAndSet(null, Thread。currentThread())) {

// loop

}

}

privatevoidunlock() {

Thread thread = Thread。currentThread();

atomicReference。compareAndSet(thread, null);

System。out。println(thread + “ unlock。。。”);

}

publicstaticvoidmain(String[] args) throws InterruptedException {

SpinLock spinLock = new SpinLock();

new Thread(() -> {

spinLock。lock();

try {

Thread。sleep(3000);

} catch (InterruptedException e) {

e。printStackTrace();

}

System。out。println(“hahaha”);

spinLock。unlock();

})。start();

Thread。sleep(1);

new Thread(() -> {

spinLock。lock();

System。out。println(“hehehe”);

spinLock。unlock();

})。start();

}

}

獲取鎖的時候,如果原子引用為空就獲取鎖,不為空表示其他執行緒獲取了鎖,就迴圈等待。

4.獨佔鎖(寫鎖)/共享鎖(讀鎖)

獨佔鎖:指該鎖一次只能被一個執行緒持有

共享鎖:該鎖可以被多個執行緒持有

對於 ReentrantLock 和 synchronized 都是獨佔鎖;對與 ReentrantReadWriteLock 其讀鎖是共享鎖而寫鎖是獨佔鎖。讀鎖的共享可保證併發讀是非常高效的,讀寫、寫讀和寫寫的過程是互斥的。

具體的例子見我之前寫過的部落格

https://blog。csdn。net/weixin_43395911/article/details/104604784

CountDownLatch/CyclicBarrier/Semaphore 使用過嗎?

1。CountDownLatch

讓一些執行緒堵塞直到另一個執行緒完成一系列操作後才被喚醒。CountDownLatch 主要有兩個方法,當一個或多個執行緒呼叫 await 方法時,呼叫執行緒會被堵塞,其他執行緒呼叫 countDown 方法會將計數減一(呼叫 countDown 方法的執行緒不會堵塞),當計數其值變為零時,因呼叫 await 方法被堵塞的執行緒會被喚醒,繼續執行。

2。CyclicBarrier

我們假設有這麼一個場景,每輛車只能坐個人,當車滿了,就發車。

3。Semaphore

假設我們有 3 個停車位,6 輛車去搶。

具體案例可以看我之前寫的部落格

https://blog。csdn。net/weixin_43395911/article/details/104604784

堵塞佇列

ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)對元素進行排序。

LinkedBlokcingQueue:是一個基於連結串列結構的阻塞佇列,此佇列按 FIFO(先進先出)對元素進行排序,吞吐量通常要高於 ArrayBlockingQueue。

SynchronousQueue:是一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於 LinkedBlokcingQueue。

阻塞佇列,顧名思義,首先它是一個佇列,而一個阻塞佇列在資料結構中所起的作用大致如圖所示:

當阻塞佇列是空時,從佇列中獲取元素的操作將會被阻塞。

當阻塞佇列是滿時,往佇列裡新增元素的操作將會被阻塞。

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

核心方法

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

行為解釋:

拋異常:如果操作不能馬上進行,則丟擲異常

特定的值:如果操作不能馬上進行,將會返回一個特殊的值,一般是 true 或者 false

阻塞:如果操作不能馬上進行,操作會被阻塞

超時:如果操作不能馬上進行,操作會被阻塞指定的時間,如果指定時間沒執行,則返回一個特殊值,一般是 true 或者 false

插入方法:

add(E e):新增成功返回true,失敗拋 IllegalStateException 異常

offer(E e):成功返回 true,如果此佇列已滿,則返回 false

put(E e):將元素插入此佇列的尾部,如果該佇列已滿,則一直阻塞

刪除方法:

remove(Object o) :移除指定元素,成功返回true,失敗返回false

poll():獲取並移除此佇列的頭元素,若佇列為空,則返回 null

take():獲取並移除此佇列頭元素,若沒有元素則一直阻塞

檢查方法:

element() :獲取但不移除此佇列的頭元素,沒有元素則拋異常

peek() :獲取但不移除此佇列的頭;若佇列為空,則返回 null

SynchronousQueue

SynchronousQueue,實際上它不是一個真正的佇列,因為它不會為佇列中元素維護儲存空間。與其他佇列不同的是,它維護一組執行緒,這些執行緒在等待著把元素加入或移出佇列。

new Thread(() -> {

try {

Integer val = synchronousQueue。take();

System。out。println(val);

Integer val2 = synchronousQueue。take();

System。out。println(val2);

Integer val3 = synchronousQueue。take();

System。out。println(val3);

} catch (InterruptedException e) {

e。printStackTrace();

}

})。start();

}

}

使用場景:

生產者消費者模式、執行緒池、訊息中介軟體。

synchronized 和 Lock 有什麼區別?

原始結構

synchronized 是關鍵字屬於 JVM 層面,反應在位元組碼上是 monitorenter 和 monitorexit,其底層是透過 monitor 物件來完成,其實 wait/notify 等方法也是依賴 monitor 物件只有在同步快或方法中才能呼叫 wait/notify 等方法。

Lock 是具體類(java。util。concurrent。locks。Lock)是 api 層面的鎖。

使用方法

synchronized 不需要使用者手動去釋放鎖,當 synchronized 程式碼執行完後系統會自動讓執行緒釋放對鎖的佔用。

ReentrantLock 則需要使用者手動的釋放鎖,若沒有主動釋放鎖,可能導致出現死鎖的現象,lock() 和 unlock() 方法需要配合 try/finally 語句來完成。

等待是否可中斷

synchronized 不可中斷,除非丟擲異常或者正常執行完成。

ReentrantLock 可中斷,設定超時方法 tryLock(long timeout, TimeUnit unit),lockInterruptibly() 放程式碼塊中,呼叫 interrupt() 方法可中斷。

加鎖是否公平

synchronized 非公平鎖。

ReentrantLock 預設非公平鎖,構造方法中可以傳入 boolean 值,true 為公平鎖,false 為非公平鎖。

鎖可以繫結多個 Condition

synchronized 沒有 Condition。

ReentrantLock 用來實現分組喚醒需要喚醒的執行緒們,可以精確喚醒,而不是像 synchronized 要麼隨機喚醒一個執行緒要麼喚醒全部執行緒。

執行緒池

1.為什麼使用執行緒池,執行緒池的優勢?

執行緒池用於多執行緒處理中,它可以根據系統的情況,可以有效控制執行緒執行的數量,最佳化執行效果。執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務放入佇列,然後線上程建立後啟動這些任務,如果執行緒數量超過了最大數量,那麼超出數量的執行緒排隊等候,等其它執行緒執行完畢,再從佇列中取出任務來執行。

主要特點為:執行緒複用、控制最大併發數量、管理執行緒。

主要優點:

降低資源消耗,透過重複利用已建立的執行緒來降低執行緒建立和銷燬造成的消耗。

提高相應速度,當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。

提高執行緒的可管理性,執行緒是稀缺資源,如果無限制的建立,不僅僅會消耗系統資源,還會降低體統的穩定性,使用執行緒可以進行統一分配,調優和監控。

2.執行緒池如何使用?

架構說明

2 萬字長文詳解 10 大多執行緒面試題|原力計劃

編碼實現

Executors。newSingleThreadExecutor():只有一個執行緒的執行緒池,因此所有提交的任務是順序執行。

Executors。newCachedThreadPool():執行緒池裡有很多執行緒需要同時執行,老的可用執行緒將被新的任務觸發重新執行,如果執行緒超過60秒內沒執行,那麼將被終止並從池中刪除。

Executors。newFixedThreadPool():擁有固定執行緒數的執行緒池,如果沒有任務執行,那麼執行緒會一直等待。

Executors。newScheduledThreadPool():用來排程即將執行的任務的執行緒池。

Executors。newWorkStealingPool():newWorkStealingPool適合使用在很耗時的操作,但是newWorkStealingPool不是ThreadPoolExecutor的擴充套件,它是新的執行緒池類ForkJoinPool的擴充套件,但是都是在統一的一個Executors類中實現,由於能夠合理的使用CPU進行對任務操作(並行操作),所以適合使用在很耗時的任務中。

ThreadPoolExecutor

ThreadPoolExecutor作為java。util。concurrent包對外提供基礎實現,以內部執行緒池的形式對外提供管理任務執行,執行緒排程,執行緒池管理等等服務。

執行緒池的幾個重要引數介紹和底層原理參見我之前的部落格:

https://blog。csdn。net/weixin_43395911/article/details/104625100

執行緒池用過嗎?生產上你如何設定合理引數?執行緒池的拒絕策略你談談?

等待佇列已經滿了,再也塞不下新的任務,同時執行緒池中的執行緒數達到了最大執行緒數,無法繼續為新任務服務。

拒絕策略

AbortPolicy:處理程式遭到拒絕將丟擲執行時 RejectedExecutionException。

CallerRunsPolicy:執行緒呼叫執行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。

DiscardPolicy:不能執行的任務將被刪除。

DiscardOldestPolicy:如果執行程式尚未關閉,則位於工作佇列頭部的任務將被刪除,然後重試執行程式(如果再次失敗,則重複此過程)

你在工作中單一的、固定數的和可變的三種建立執行緒池的方法,你用哪個多,超級大坑?

如果讀者對Java中的阻塞佇列有所瞭解的話,看到這裡或許就能夠明白原因了。

Java中的BlockingQueue主要有兩種實現,分別是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一個用陣列實現的有界阻塞佇列,必須設定容量。

LinkedBlockingQueue是一個用連結串列實現的有界阻塞佇列,容量可以選擇進行設定,不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer。MAX_VALUE。

這裡的問題就出在:不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer。MAX_VALUE。也就是說,如果我們不設定LinkedBlockingQueue的容量的話,其預設容量將會是Integer。MAX_VALUE。

而newFixedThreadPool中建立LinkedBlockingQueue時,並未指定容量。此時,LinkedBlockingQueue就是一個無邊界佇列,對於一個無邊界佇列來說,是可以不斷的向佇列中加入任務的,這種情況下就有可能因為任務過多而導致記憶體溢位問題。

上面提到的問題主要體現在newFixedThreadPool和newSingleThreadExecutor兩個工廠方法上,並不是說newCachedThreadPool和newScheduledThreadPool這兩個方法就安全了,這兩種方式建立的最大執行緒數可能是Integer。MAX_VALUE,而建立這麼多執行緒,必然就有可能導致OOM。

合理配置執行緒池你是如果考慮的?

1、CPU 密集型

CPU 密集的意思是該任務需要大量的運算,而沒有阻塞,CPU 一直全速執行。

CPU 密集型任務儘可能的少的執行緒數量,一般為 CPU 核數 + 1 個執行緒的執行緒池。

2、IO 密集型

由於 IO 密集型任務執行緒並不是一直在執行任務,可以多分配一點執行緒數,如 CPU * 2 。

也可以使用公式:CPU 核數 / (1 - 阻塞係數);其中阻塞係數在 0。8 ~ 0。9 之間。

死鎖編碼以及定位分析

產生死鎖的原因:

死鎖是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種相互等待的現象,如果無外力的干涉那它們都將無法推進下去,如果系統的資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。

publicclassDeadLockDemo {

publicstaticvoidmain(String[] args) {

String lockA = “lockA”;

String lockB = “lockB”;

DeadLockDemo deadLockDemo = new DeadLockDemo();

Executor executor = Executors。newFixedThreadPool(2);

executor。execute(() -> deadLockDemo。method(lockA, lockB));

executor。execute(() -> deadLockDemo。method(lockB, lockA));

}

publicvoidmethod(String lock1, String lock2) {

synchronized (lock1) {

System。out。println(Thread。currentThread()。getName() + “——獲取到:” + lock1 + “; 嘗試獲取:” + lock2);

try {

Thread。sleep(1000);

} catch (InterruptedException e) {

e。printStackTrace();

}

synchronized (lock2) {

System。out。println(“獲取到兩把鎖!”);

}

}

}

}

解決:

jps -l 命令查定位程序號

28519 org。jetbrains。jps。cmdline。Launcher

32376 com。intellij。idea。Main

28521 com。cuzz。thread。DeadLockDemo

27836 org。jetbrains。kotlin。daemon。KotlinCompileDaemon

28591 sun。tools。jps。Jps

jstack 28521 找到死鎖檢視:

2019-05-07 00:04:15

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25。191-b12 mixed mode):

“Attach Listener” #13 daemon prio=9 os_prio=0 tid=0x00007f7acc001000 nid=0x702a waiting on condition [0x0000000000000000]

java。lang。Thread。State: RUNNABLE

// 。。。

Found one Java-level deadlock:

=============================

“pool-1-thread-2”:

waiting to lock monitor 0x00007f7ad4006478 (object0x00000000d71f60b0, a java。lang。String),

which is held by“pool-1-thread-1”

“pool-1-thread-1”:

waiting tolock monitor 0x00007f7ad4003be8 (object0x00000000d71f60e8, a java。lang。String),

which is held by“pool-1-thread-2”

Java stack information for the threads listed above:

===================================================

“pool-1-thread-2”:

at com。cuzz。thread。DeadLockDemo。method(DeadLockDemo。java:34)

- waiting tolock <0x00000000d71f60b0> (a java。lang。String)

- locked <0x00000000d71f60e8> (a java。lang。String)

at com。cuzz。thread。DeadLockDemo。lambda$main$1(DeadLockDemo。java:21)

at com。cuzz。thread。DeadLockDemo$$Lambda$2/2074407503。run(UnknownSource)

at java。util。concurrent。ThreadPoolExecutor。runWorker(ThreadPoolExecutor。java:1149)

at java。util。concurrent。ThreadPoolExecutor$Worker。run(ThreadPoolExecutor。java:624)

at java。lang。Thread。run(Thread。java:748)

“pool-1-thread-1”:

at com。cuzz。thread。DeadLockDemo。method(DeadLockDemo。java:34)

- waiting tolock <0x00000000d71f60e8> (a java。lang。String)

- locked <0x00000000d71f60b0> (a java。lang。String)

at com。cuzz。thread。DeadLockDemo。lambda$main$0(DeadLockDemo。java:20)

at com。cuzz。thread。DeadLockDemo$$Lambda$1/558638686。run(UnknownSource)

at java。util。concurrent。ThreadPoolExecutor。runWorker(ThreadPoolExecutor。java:1149)

at java。util。concurrent。ThreadPoolExecutor$Worker。run(ThreadPoolExecutor。java:624)

at java。lang。Thread。run(Thread。java:748)

Found1 deadlock。

最後發現一個死鎖。

本文是我在B站上學習面試題的總結,希望大家可以點贊收藏,這些面試題都是大廠必問的,祝願各位都能找到心儀的工作!

原文連結:

https://blog。csdn。net/weixin_43395911/article/details/104660403

版權宣告:本文為CSDN博主「ZZZhonngger」的原創文章,遵循 CC 4。0 BY-SA 版權協議,轉載請附上原文出處連結及本宣告。

【End】