2 萬字長文詳解 10 大多線程面試題|原力計劃

2 萬字長文詳解 10 大多線程面試題|原力計劃

作者 | ZZZhonngger

出品 | CSDN博客

2 万字长文详解 10 大多线程面试题|原力计划

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
*/
public class VolatileDemo {
public static void main(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");
}
}
class MyData {
//int a = 0;
volatile int a = 0;
void addOne {
this.a += 1;
}
}

如果不加 volatile 關鍵字,則主線程會進入死循環,加 volatile 則主線程能夠退出,說明加了 volatile 關鍵字變量,當有一個線程修改了值,會馬上被另一個線程感知到,當前值作廢,從新從主內存中獲取值。對其他線程可見,這就叫可見性。

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

public class VolatileDemo {
public static void main(String[] args) {
test2;
}
public static void test2{
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);
}
}
class MyData {
//int a = 0;
volatile int a = 0;
void addOne {
this.a += 1;
}
}

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

class Test{

volatile int n=0;

public void add{

n++;

}

}

編譯成字節碼文件是這樣的:

2 万字长文详解 10 大多线程面试题|原力计划

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

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

AtomicInteger atomicInteger=new AtomicInteger;//默認為0
public void addAtomic{
atomicInteger.incrementAndGet;//相當於n++
}

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

2 万字长文详解 10 大多线程面试题|原力计划

volatile 實現禁止指令重排序的優化,從而避免了多線程環境下程序出現亂序的現象。

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

  • 保證特定操作的執行順序

  • 保證某些變量的內存可見性(利用該特性實現 volatile 的內存可見性)

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

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

2 万字长文详解 10 大多线程面试题|原力计划

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

線程安全性保證:

  • 工作內存與主內存同步延遲現象導致可見性問題:可以使用 synchronzied 或 volatile 關鍵字解決,它們可以使用一個線程修改後的變量立即對其他線程可見

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

2、你在哪些地方用到過 volatile?單例模式(DCL,double check lock雙端檢鎖機制)

//volatile是輕量級Synchronized,保證內存的可見性,防止指令重排序
class SingleTon {
private static volatile SingleTon singleTon;

private SingleTon {
}

//解決線程安全問題,同時解決懶加載問題,也保證了效率
public static synchronized SingleTon getSingleTon {

if (singleTon == ) {
//同步代碼效率較低
synchronized (SingleTon.class) {
if (singleTon == ) {
singleTon = new SingleTon;
}
}
}
return singleTon;
}
}

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

instance = new Singleton

可以分為以下三步完成(偽代碼):

memory = allocate; // 1.分配對象空間
instance(memory); // 2.初始化對象
instance = memory; // 3.設置instance指向剛分配的內存地址,此時instance !=

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

memory = allocate; // 1.分配對象空間 

instance = memory; // 3.設置instance指向剛分配的內存地址,此時instance != ,但對象還沒有初始化完成
instance(memory); // 2.初始化對象

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

2 万字长文详解 10 大多线程面试题|原力计划

CAS相關

1.先看一個小Demo

package Day35;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @Author Zhongger
* @Description CAS算法——比較和交換

* @Date 2020.3.5
*/
public class CASDemo {
public static void main(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 int getAndIncrement {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

發現調用了unsafe類的方法。

這是AtomicInteger類的部分源碼:

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe;
private static final long valueOffset;

static {
try {
// 獲取下面 value 的地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
// ......
}

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

private static 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為修改的數值
public final int getAndAddInt(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 問題。

2 万字长文详解 10 大多线程面试题|原力计划

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
*/
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<user> 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
}
}

class User{
private String username;
private int age;

public User(String username, int age) {
this.username = username;
this.age = age;
}

public String getUsername {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public int getAge {
return age;
}


public void setAge(int age) {
this.age = age;
}

@Override
public String toString {
return "User{" +
"username='" + username + '\\'' +
", age=" + age +
'}';
}
}
/<user>
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
*/
public class ABAProblem {
private static AtomicReference<integer> atomicReference=new AtomicReference<>(100);

public static void main(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;
}
}/<integer>

結果為:

2 万字长文详解 10 大多线程面试题|原力计划

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

package Day37;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
* @Author Zhongger
* @Description 使用AtomicStampedReference來解決ABA問題
* @Date 2020.3.7
*/
public class ABASolution {
private static AtomicStampedReference<integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
//主內存中初始值為100,版本號為1
public static void main(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;
}

}/<integer>

運行結果如下:

2 万字长文详解 10 大多线程面试题|原力计划

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

2 万字长文详解 10 大多线程面试题|原力计划

集合類線程

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

public class ArrayListDemo {
public static void main(String[] args) {
List<integer> 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;
}
}
}/<integer>

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

解決方案

  • new Vector;

  • Collections.synchronizedList(new ArrayList<>);

  • new CopyOnWriteArrayList<>;

優化建議

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

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

2 万字长文详解 10 大多线程面试题|原力计划

Java中的鎖

1.公平和非公平鎖

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

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

兩者區別:

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

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

2.可重入鎖和不可重入鎖可重入鎖:也叫遞歸鎖,指的是同一個線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取到該鎖,在同一個線程在外層方法獲取鎖的時候,在進入內層方法或會自動獲取該鎖。可重入鎖的最大作用是避免死鎖。不可重入鎖:所謂不可重入鎖,即若當前線程執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。

package Day38;

/**
* @Author Zhongger
* @Description 可重入鎖演示
* @Date 2020.3.8
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
Phone phone = new Phone;
new Thread(->{
phone.sendSMS;
},"A").start;

new Thread(->{
phone.sendSMS;
},"B").start;
}
}
//資源類
class Phone{
public synchronized void sendSMS{
System.out.println(Thread.currentThread.getName + "\\t 執行了sendSMS方法");
sendEmail;
//sendSMS方法調用了加了鎖的sendEmail方法,如果Thread.currentThread.getName是一致的
//說明synchronized是可重入鎖
}
public synchronized void sendEmail{
System.out.println(Thread.currentThread.getName + "\\t 執行了sendEmail方法");
}
}

運行結果如下:

2 万字长文详解 10 大多线程面试题|原力计划

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

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

手寫一個可重入鎖:

public class ReentrantLock {
boolean isLocked = false;
Thread lockedBy = ;
int lockedCount = 0;
public synchronized void lock throws InterruptedException {
Thread thread = Thread.currentThread;
while (isLocked && lockedBy != thread) {
wait;
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}

public synchronized void unlock {
if (Thread.currentThread == lockedBy) {
lockedCount--;
if (lockedCount == 0) {
isLocked = false;
notify;
}
}
}
}

測試:

public class Count {
ReentrantLock lock = new ReentrantLock;
public void print throws InterruptedException{
lock.lock;
doAdd;
lock.unlock;
}

private void doAdd throws InterruptedException {

lock.lock;
// do something
System.out.println("ReentrantLock");
lock.unlock;
}

public static void main(String[] args) throws InterruptedException {
Count count = new Count;
count.print;
}
}

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

手寫一個不可重入鎖:

public class NotReentrantLock {
private boolean isLocked = false;
public synchronized void lock throws InterruptedException {
while (isLocked) {
wait;
}
isLocked = true;
}
public synchronized void unlock {
isLocked = false;
notify;
}
}

測試:

public class Count {
NotReentrantLock lock = new NotReentrantLock;
public void print throws InterruptedException{
lock.lock;
doAdd;
lock.unlock;
}

private void doAdd throws InterruptedException {
lock.lock;
// do something
lock.unlock;
}

public static void main(String[] args) throws InterruptedException {
Count count = new Count;
count.print;
}
}

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

3.自旋鎖

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

手動實現自旋鎖

public class SpinLock {
private AtomicReference<thread> atomicReference = new AtomicReference<>;
private void lock {
System.out.println(Thread.currentThread + " coming...");
while (!atomicReference.compareAndSet(, Thread.currentThread)) {
// loop

}
}

private void unlock {
Thread thread = Thread.currentThread;
atomicReference.compareAndSet(thread, );
System.out.println(thread + " unlock...");
}

public static void main(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;
}
}/<thread>

獲取鎖的時候,如果原子引用為空就獲取鎖,不為空表示其他線程獲取了鎖,就循環等待。

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

獨佔鎖:指該鎖一次只能被一個線程持有

共享鎖:該鎖可以被多個線程持有

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

具體的例子見我之前寫過的博客

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

2 万字长文详解 10 大多线程面试题|原力计划

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

2 万字长文详解 10 大多线程面试题|原力计划

堵塞隊列

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:獲取並移除此隊列的頭元素,若隊列為空,則返回

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

檢查方法:

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

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

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;
}
}

使用場景:

生產者消費者模式、線程池、消息中間件。

2 万字长文详解 10 大多线程面试题|原力计划

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 要麼隨機喚醒一個線程要麼喚醒全部線程。

2 万字长文详解 10 大多线程面试题|原力计划

線程池

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 之間。

2 万字长文详解 10 大多线程面试题|原力计划

死鎖編碼以及定位分析

產生死鎖的原因:

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

public class DeadLockDemo {
public static void main(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));

}

public void method(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 (object 0x00000000d71f60b0, a java.lang.String),
which is held by "pool-1-thread-1"
"pool-1-thread-1":
waiting to lock monitor 0x00007f7ad4003be8 (object 0x00000000d71f60e8, 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 to lock <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(Unknown Source)
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 to lock <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(Unknown Source)
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)

Found 1 deadlock.

最後發現一個死鎖。

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

原文鏈接:

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


分享到:


相關文章: