文章目錄
- 多執行緒開發實體
- 應用背景
- 設計要點
- 防止重復
- 失敗機制
- 執行緒池選擇
- 核心代碼
- 對線面試官
- 面試官:先從最簡單的開始,說說什么是執行緒吧
- 面試官:說說Java里怎么創建執行緒吧
- 面試官:說說執行緒的生命周期和狀態
- 面試官:我看你提到了執行緒阻塞,那你再說說執行緒死鎖吧
- 面試官:怎么避免死鎖呢?
- 面試官:我看你的例子里用到了synchronized,說說 synchronized的用法吧
- 面試官:除了使用synchronized,還有什么辦法來加鎖嗎?詳細說一下
- 面試官:說說synchronized和Lock的區別
- 面試官:你提到了synchronized基于jvm層面,對這個有了解嗎?
- synchronized的優化能說一說嗎?
- 面試官:說一下CAS
- 面試官:CAS會導致什么問題?
- 面試官:能說一下說下ReentrantLock原理嗎
- 面試官:能說一下AQS嗎
- 面試官:能說一下Semaphore/CountDownLatch/CyclicBarrier嗎
- volatile原理知道嗎?
- 面試官:說說你對Java記憶體模型(JMM)的理解,為什么要用JMM
- 面試官:看你用到了執行緒池,能說說為什么嗎
- 面試官:能說一下執行緒池的核心引數嗎?
- 面試官:完整說一下執行緒池的作業流程
- 面試官:拒絕策略有哪些
- 面試官:說一下你的核心執行緒數是怎么選的
- 面試官:說一下有哪些常見阻塞佇列
- 面試官:說一下有哪幾種常見的執行緒池吧
在面試當中,有時候會問到你在專案中用過多執行緒么?
對于普通的應屆生或者作業時間不長的初級開發 ???—— crud仔流下了沒有技術的眼淚,
博主這里整理了專案中用到了多執行緒的一個簡單的實體,希望能對你有所啟發,
多執行緒開發實體
應用背景
應用的背景非常簡單,博主做的專案是一個審核類的專案,審核的資料需要推送給第三方監管系統,這只是一個很簡單的對接,但是存在一個問題,
我們需要推送的資料大概三十萬條,但是第三方監管提供的介面只支持單條推送(別問為什么不支持批量,問就是沒討撕論比好過),可以估算一下,三十萬條資料,一條資料按3秒算,大概需要250(為什么恰好會是這個數)個小時,
所以就考慮到引入多執行緒來進行并發操作,降低資料推送的時間,提高資料推送的實時性,

設計要點
防止重復
我們推送給第三方的資料肯定是不能重復推送的,必須要有一個機制保證各個執行緒推送資料的隔離,
這里有兩個思路:
-
- 將所有資料取到集合(記憶體)中,然后進行切割,每個執行緒推送不同段的資料
-
- 利用 資料庫分頁的方式,每個執行緒取 [start,limit] 區間的資料推送,我們需要保證start的一致性
這里采用了第二種方式,因為考慮到可能資料量后續會繼續增加,把所有資料都加載到記憶體中,可能會有比較大的記憶體占用,
失敗機制
我們還得考慮到執行緒推送資料失敗的情況,
如果是自己的系統,我們可以把多執行緒呼叫的方法抽出來加一個事務,一個執行緒例外,整體回滾,
但是是和第三方的對接,我們都沒法做事務的,所以,我們采用了直接在資料庫記錄失敗狀態的方法,可以在后面用其它方式處理失敗的資料,
執行緒池選擇
在實際使用中,我們肯定是要用到執行緒池來管理執行緒,關于執行緒池,我們常用 ThreadPoolExecutor提供的執行緒池服務,SpringBoot中同樣也提供了執行緒池異步的方式,雖然SprignBoot異步可能更方便一點,但是使用ThreadPoolExecutor更加直觀地控制執行緒池,所以我們直接使用ThreadPoolExecutor構造方法創建執行緒池,
大概的技術設計示意圖:

核心代碼
上面叭叭了一堆,到了show you code的環節了,我將專案里的代碼抽取出來,簡化出了一個示例,
核心代碼如下:
/**
* @Author 三分惡
* @Date 2021/3/5
* @Description
*/
@Service
public class PushProcessServiceImpl implements PushProcessService {
@Autowired
private PushUtil pushUtil;
@Autowired
private PushProcessMapper pushProcessMapper;
private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);
//每個執行緒每次查詢的條數
private static final Integer LIMIT = 5000;
//起的執行緒數
private static final Integer THREAD_NUM = 5;
//創建執行緒池
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
@Override
public void pushData() throws ExecutionException, InterruptedException {
//計數器,需要保證執行緒安全
int count = 0;
//未推送資料總數
Integer total = pushProcessMapper.countPushRecordsByState(0);
logger.info("未推送資料條數:{}", total);
//計算需要多少輪
int num = total / (LIMIT * THREAD_NUM) + 1;
logger.info("要經過的輪數:{}", num);
//統計總共推送成功的資料條數
int totalSuccessCount = 0;
for (int i = 0; i < num; i++) {
//接收執行緒回傳結果
List<Future<Integer>> futureList = new ArrayList<>(32);
//起THREAD_NUM個執行緒并行查詢更新庫,加鎖
for (int j = 0; j < THREAD_NUM; j++) {
synchronized (PushProcessServiceImpl.class) {
int start = count * LIMIT;
count++;
//提交執行緒,用資料起始位置標識執行緒
Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
//先不取值,防止阻塞,放進集合
futureList.add(future);
}
}
//統計本輪推送成功資料
for (Future f : futureList) {
totalSuccessCount = totalSuccessCount + (int) f.get();
}
}
//更新推送標志
pushProcessMapper.updateAllState(1);
logger.info("推送資料完成,需推送資料:{},推送成功:{}", total, totalSuccessCount);
}
/**
* 推送資料執行緒類
*/
class PushDataTask implements Callable<Integer> {
int start;
int limit;
int threadNo; //執行緒編號
PushDataTask(int start, int limit, int threadNo) {
this.start = start;
this.limit = limit;
this.threadNo = threadNo;
}
@Override
public Integer call() throws Exception {
int count = 0;
//推送的資料
List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
if (CollectionUtils.isEmpty(pushProcessList)) {
return count;
}
logger.info("執行緒{}開始推送資料", threadNo);
for (PushProcess process : pushProcessList) {
boolean isSuccess = pushUtil.sendRecord(process);
if (isSuccess) { //推送成功
//更新推送標識
pushProcessMapper.updateFlagById(process.getId(), 1);
count++;
} else { //推送失敗
pushProcessMapper.updateFlagById(process.getId(), 2);
}
}
logger.info("執行緒{}推送成功{}條", threadNo, count);
return count;
}
}
}
代碼很長,我們簡單說一下關鍵的地方:
- 執行緒創建:執行緒內部類選擇了實作Callable介面,這樣方便獲取執行緒任務執行的結果,在示例里用于統計執行緒推送成功的數量
class PushDataTask implements Callable<Integer> {
- 使用 ThreadPoolExecutor 創建執行緒池,
//創建執行緒池
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
主要構造引數如下:
? - corePoolSize:執行緒核心引數選擇了5
? - maximumPoolSize:最大執行緒數選擇了核心執行緒數2倍數
? - keepAliveTime:非核心閑置執行緒存活時間直接置為0
? - unit:非核心執行緒保持存活的時間選擇了 TimeUnit.SECONDS 秒
? - workQueue:執行緒池等待佇列,使用 容量初始為100的 LinkedBlockingQueue阻塞佇列
這里還有沒寫出來的執行緒池拒絕策略,采用了默認AbortPolicy:直接丟棄任務,拋出例外,
- 使用 synchronized 來保證執行緒安全,保證計數器的增加是有序的
synchronized (PushProcessServiceImpl.class) {
- 使用集合來接收執行緒的運行結果,防止阻塞
List<Future<Integer>> futureList = new ArrayList<>(32);
好了,主要的代碼和簡單的決議就到這里了,
關于這個簡單的demo,這里只是簡單地做推送資料處理,考慮一下,這個實體是不是可以用在你專案的某些地方,例如監管系統的資料校驗、審計系統的資料統計、電商系統的資料分析等等,只要是有大量資料處理的地方,都可以把這個例子結合到你的專案里,這樣你就有了多執行緒開發的經驗,
完整代碼倉庫地址在文章底部👇👇
對線面試官
- 面試官:小伙子,不錯,你這個整挺好,
- 老三:那是自然,
- 面試官:呦,小伙子,挺自信,那我得好好考考你,
- 老三:放馬過來,但考無妨,
面試官:先從最簡單的開始,說說什么是執行緒吧
要說執行緒,必先說行程,
行程是程式的?次執?程序,是系統運?程式的基本單位,因此行程是動態的,系統運??個程式即是?個行程從創建,運?到消亡的程序,
執行緒與行程相似,但執行緒是?個?行程更?的執?單位,?個行程在其執?的程序中可以產?多個執行緒,與行程不同的是同類的多個執行緒共享行程的堆和?法區資源,但每個執行緒有??的程式計數器、虛擬機堆疊和本地?法堆疊,所以系統在產??個執行緒,或是在各個執行緒之間作切換?作時,負擔要?行程?得多,也正因為如此,執行緒也被稱為輕量級行程,
面試官:說說Java里怎么創建執行緒吧
Java里創建執行緒主要有三種方式:
-
繼承 Thread類:Thread 類本質上是實作了 Runnable 介面的一個實體,代表一個執行緒的實體,啟動執行緒的唯一方法就是通過 Thread 類的 start()實體方法,start()方法是一個 native 方法,它將啟動一個新執行緒,并執行 run()方法,
-
實作 Runnable介面:如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實作一個Runnable 介面,
-
實作 Callable介面:實作Callable介面,重寫call()方法,可以回傳一個 Future型別的回傳值,我在上面的例子里就是用到了這種方式,
面試官:說說執行緒的生命周期和狀態
在Java中,執行緒共有六種狀態:
| 狀態 | 說明 |
|---|---|
| NEW | 初始狀態:執行緒被創建,但還沒有呼叫start()方法 |
| RUNNABLE | 運行狀態:Java執行緒將作業系統中的就緒和運行兩種狀態籠統的稱作“運行” |
| BLOCKED | 阻塞狀態:表示執行緒阻塞于鎖 |
| WAITING | 等待狀態:表示執行緒進入等待狀態,進入該狀態表示當前執行緒需要等待其他執行緒做出一些特定動作(通知或中斷) |
| TIME_WAITING | 超時等待狀態:該狀態不同于 WAITIND,它是可以在指定的時間自行回傳的 |
| TERMINATED | 終止狀態:表示當前執行緒已經執行完畢 |
執行緒在自身的生命周期中, 并不是固定地處于某個狀態,而是隨著代碼的執行在不同的狀態之間進行切換,Java執行緒狀態變化如圖示:

面試官:我看你提到了執行緒阻塞,那你再說說執行緒死鎖吧
執行緒死鎖描述的是這樣?種情況:多個執行緒同時被阻塞,它們中的?個或者全部都在等待某個資源被釋放,由于執行緒被?限期地阻塞,因此程式不可能正常終?,
如下圖所示,執行緒 A 持有資源 2,執行緒 B 持有資源 1,他們同時都想申請對?的資源,所以這兩個執行緒就會互相等待?進?死鎖狀態,
產生死鎖必須滿足四個條件:
-
互斥條件:該資源任意?個時刻只由?個執行緒占?,
-
請求與保持條件:?個行程因請求資源?阻塞時,對已獲得的資源保持不放,
-
不剝奪條件:執行緒已獲得的資源在末使?完之前不能被其他執行緒強?剝奪,只有??使?完畢后才釋放資源,
-
回圈等待條件:若?行程之間形成?種頭尾相接的回圈等待資源關系,
面試官:怎么避免死鎖呢?
我上?說了產?死鎖的四個必要條件,為了避免死鎖,我們只要破壞產?死鎖的四個條件中的其中?個就可以了,
-
破壞互斥條件 :這個條件我們沒有辦法破壞,因為我們?鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問),
-
破壞請求與保持條件 :?次性申請所有的資源,
-
破壞不剝奪條件 :占?部分資源的執行緒進?步申請其他資源時,如果申請不到,可以主動釋放它占有的資源,
-
破壞回圈等待條件 :靠按序申請資源來預防,按某?順序申請資源,釋放資源則反序釋放,破壞回圈等待條件,
面試官:我看你的例子里用到了synchronized,說說 synchronized的用法吧
synchronized 關鍵字最主要的三種使??式:
1.修飾實體?法: 作?于當前物件實體加鎖,進?同步代碼前要獲得 當前物件實體的鎖
synchronized void method() {
//業務代碼
}
2.修飾靜態?法: 也就是給當前類加鎖,會作?于類的所有物件實體 ,進?同步代碼前要獲得當前 class 的鎖,因為靜態成員不屬于任何?個實體物件,是類成員( static 表明這是該類的?個靜態資源,不管 new 了多少個物件,只有?份),所以,如果?個執行緒 A 調??個實體物件的?靜態 synchronized ?法,?執行緒 B 需要調?這個實體物件所屬類的靜態 synchronized ?法,是允許的,不會發?互斥現象,因為訪問靜態 synchronized ?法占?的鎖是當前類的鎖,?訪問?靜態 synchronized ?法占?的鎖是當前實體物件鎖,
synchronized void staic method() {
//業務代碼
}
**3.**修飾代碼塊 :指定加鎖物件,對給定物件/類加鎖, synchronized(this|object) 表示進?同步代碼庫前要獲得給定物件的鎖, synchronized(類.class) 表示進?同步代碼前要獲得 當前 class 的鎖
synchronized(this) {
//業務代碼
}
在我的例子里使用synchronized修飾代碼塊,給PushProcessServiceImpl類加鎖,進?同步代碼前要獲得 當前 class 的鎖,防止PushProcessServiceImpl類的物件在控制層呼叫推送資料的方法,
面試官:除了使用synchronized,還有什么辦法來加鎖嗎?詳細說一下
可以使用juc包提供的鎖,Lock介面主要相關的類和介面如下,

Lock中的主要方法:
- lock:用來獲取鎖,如果鎖被其他執行緒獲取,進入等待狀態,
- lockInterruptibly:通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠回應中斷,即中斷執行緒的等待狀態,
- tryLock:tryLock方法是有回傳值的,它表示用來嘗試獲取鎖,如果獲取成功,則回傳true,如果獲取失敗(即鎖已被其他執行緒獲取),則回傳false,
- tryLock(long,TimeUnit):與tryLock類似,只不過是有等待時間,在等待時間內獲取到鎖回傳true,超時回傳false,
- unlock:釋放鎖,
其它介面和類:
- ReetrantLock(可重入鎖):實作了Lock介面,可重入鎖,內部定義了公平鎖與非公平鎖,可以完成synchronized 所能完成的所有作業,
- ReadWriteLock(讀寫鎖):
public interface ReadWriteLock {
Lock readLock(); //獲取讀鎖
Lock writeLock(); //獲取寫鎖
}
一個用來獲取讀鎖,一個用來獲取寫鎖,也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作,
- ReetrantReadWriteLock(可重入讀寫鎖):ReetrantReadWriteLock同樣支持公平性選擇,支持重進入,鎖降級,
面試官:說說synchronized和Lock的區別
| 類別 | synchronized | Lock |
|---|---|---|
| 存在層次 | Java的關鍵字,在jvm層面上 | 是一個介面,api級別 |
| 鎖的釋放 | 1、以獲取鎖的執行緒執行完同步代碼,釋放鎖 2、執行緒執行發生例外,jvm會讓執行緒釋放鎖 | 在finally中必須釋放鎖,不然容易造成執行緒死鎖 |
| 鎖的獲取 | 假設A執行緒獲得鎖,B執行緒等待,如果A執行緒阻塞,B執行緒會一直等待 | 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,執行緒可以不用一直等待 |
| 鎖狀態 | 無法判斷 | 可以判斷 |
| 鎖型別 | 可重入 不可中斷 非公平 | 可重入 可判斷 可公平(兩者皆可) |
| 性能 | 少量同步 | 大量同步 |
面試官:你提到了synchronized基于jvm層面,對這個有了解嗎?
synchronized是利用java提供的原?性內置鎖(monitor 物件),每個物件中都內置了?個 ObjectMonitor 物件,這種內置的并且使?者看不到的鎖也被稱為監視器鎖,
同步陳述句塊
synchronized 同步陳述句塊的實作使?的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代碼塊的開始位置monitorexit 指令則指明同步代碼塊的結束位置,
執?monitorenter指令時會嘗試獲取內置鎖,如果物件沒有被鎖定或者已經獲得了鎖,鎖的計數器+1,此時其他競爭鎖的執行緒則會進?等待佇列中,
執?monitorexit指令時則會把計數器-1,當計數器值為0時,則鎖釋放,處于等待佇列中的執行緒再繼續競爭鎖,
synchronized 修飾?法
synchronized 修飾的?法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是ACC_SYNCHRONIZED 標識,該標識指明了該?法是?個同步?法,JVM 通過該ACC_SYNCHRONIZED 訪問標志來辨別?個?法是否宣告為同步?法,從?執?相應的同步調?,
當然,二者細節略有不同,但本質上都是獲取原子性內置鎖,
再深入一點,synchronized實際上有兩個佇列waitSet和entryList,
-
當多個執行緒進?同步代碼塊時,?先進?entryList
-
有?個執行緒獲取到monitor鎖后,就賦值給當前執行緒,并且計數器+1
-
如果執行緒調?wait?法,將釋放鎖,當前執行緒置為null,計數器-1,同時進?waitSet等待被喚醒,調?notify或者notifyAll之后?會進?entryList競爭鎖
-
如果執行緒執?完畢,同樣釋放鎖,計數器-1,當前執行緒置為null
synchronized的優化能說一說嗎?
從JDK1.6版本之后,synchronized本身也在不斷優化鎖的機制,有些情況下他并不會是?個很重量級的鎖,優化機制包括?適應鎖、?旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖,
鎖的狀態從低到?依次為?鎖**->偏向鎖->輕量級鎖->**重量級鎖,升級的程序就是從低到?,
自旋鎖:由于?部分時候,鎖被占?的時間很短,共享變數的鎖定時間也很短,所有沒有必要掛起執行緒,?戶態和內核態的來回上下?切換嚴重影響性能,?旋的概念就是讓執行緒執??個忙回圈,可以理解為就是啥也不?,防?從?戶態轉?內核態,?旋鎖可以通過設定-XX:+UseSpining來開啟,?旋的默認次數是10次,可以使?-XX:PreBlockSpin設定,
自適應鎖:自適應鎖就是自適應的自旋鎖,自旋鎖的時間不是固定時間,而是由前?次在同?個鎖上的?旋時間和鎖的持有者狀態來決定,
鎖消除:鎖消除指的是JVM檢測到?些同步的代碼塊,完全不存在資料競爭的場景,也就是不需要加鎖,就會進?鎖消除,
鎖粗化:鎖粗化指的是有很多操作都是對同?個物件進?加鎖,就會把鎖的同步范圍擴展到整個操作序列之外,
偏向鎖:當執行緒訪問同步塊獲取鎖時,會在物件頭和堆疊幀中的鎖記錄?存盤偏向鎖的執行緒ID,之后這個執行緒再次進?同步塊時都不需要CAS來加鎖和解鎖了,偏向鎖會永遠偏向第?個獲得鎖的執行緒,如果后續沒有其他執行緒獲得過這個鎖,持有鎖的執行緒就永遠不需要進?同步,反之,當有其他執行緒競爭偏向鎖時,持有偏向鎖的執行緒就會釋放偏向鎖,可以?過設定-XX:+UseBiasedLocking開啟偏向鎖,
輕量級鎖:JVM的物件的物件頭中包含有?些鎖的標志位,代碼進?同步塊的時候,JVM將會使?CAS?式來嘗試獲取鎖,如果更新成功則會把物件頭中的狀態位標記為輕量級鎖,如果更新失敗,當前執行緒就嘗試?旋來獲得鎖,
鎖升級的程序非常復雜,簡單點說,偏向鎖就是通過物件頭的偏向執行緒ID來對?,甚?都不需要CAS了,?輕量級鎖主要就是通過CAS修改物件頭鎖記錄和?旋來實作,重量級鎖則是除了擁有鎖的執行緒其他全部阻塞,

面試官:說一下CAS
CAS(Compare And Swap/Set)比較并交換,CAS 演算法的程序是這樣:它包含 3 個引數CAS(V,E,N),V 表示要更新的變數(記憶體值),E 表示預期值(舊的),N 表示新值,當且僅當 V 值等于 E 值時,才會將 V 的值設為 N,如果 V 值和 E 值不同,則說明已經有其他執行緒做了更新,則當前執行緒什么都不做,最后,CAS 回傳當前 V 的真實值,
CAS是一種樂觀鎖,它總是認為自己可以成功完成操作,當多個執行緒同時使用 CAS 操作一個變數時,只有一個會勝出,并成功更新,其余均會失敗,失敗的執行緒不會被掛起,僅是被告知失敗,并且允許再次嘗試,當然也允許失敗的執行緒放棄操作,基于這樣的原理,CAS 操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,并進行恰當的處理,
java.util.concurrent.atomic 包下的類大多是使用 CAS 操作來實作的 (AtomicInteger,AtomicBoolean,AtomicLong),
面試官:CAS會導致什么問題?
- ABA 問題:
比如說一個執行緒 one 從記憶體位置 V 中取出 A,這時候另一個執行緒 two 也從記憶體中取出 A,并且 two 進行了一些操作變成了 B,然后 two 又將 V 位置的資料變成 A,這時候執行緒 one 進行 CAS 操作發現記憶體中仍然是 A,然后 one 操作成功,盡管執行緒 one 的 CAS 操作成功,但可能存在潛藏的問題,從 Java1.5 開始 JDK 的 atomic 包里提供了一個類 AtomicStampedReference 來解決 ABA 問題,
- 回圈時間長開銷大:
對于資源競爭嚴重(執行緒沖突嚴重)的情況,CAS 自旋的概率會比較大,從而浪費更多的 CPU 資源,效率低于 synchronized,
- 只能保證一個共享變數的原子操作:
當對一個共享變數執行操作時,我們可以使用回圈 CAS 的方式來保證原子操作,但是對多個共享變數操作時,回圈 CAS 就無法保證操作的原子性,這個時候就可以用鎖,
面試官:能說一下說下ReentrantLock原理嗎
ReentrantLock 是基于 Lock 實作的可重入鎖,所有的 Lock 都是基于 AQS 實作的,AQS 和 Condition 各自維護不同的物件,在使用 Lock 和 Condition 時,其實就是兩個佇列的互相移動,它所提供的共享鎖、互斥鎖都是基于對 state 的操作,
面試官:能說一下AQS嗎
AbstractQueuedSynchronizer,抽象的佇列式的同步器,AQS 定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實作都依賴于它,如常用的
ReentrantLock/Semaphore/CountDownLatch,
AQS 核?思想是,如果被請求的共享資源空閑,則將當前請求資源的執行緒設定為有效的?作執行緒,并且將共享資源設定為鎖定狀態,如果被請求的共享資源被占?,那么就需要?套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制 AQS 是? CLH 佇列鎖實作的,即將暫時獲取不到鎖的執行緒加?到佇列中,
看個 AQS原理圖:

AQS 使??個 int 成員變數來表示同步狀態,通過內置的 FIFO 佇列來完成獲取資源執行緒的排隊?作,AQS 使? CAS 對該同步狀態進?原?操作實作對其值的修改,
private volatile int state;//共享變數,使?volatile修飾保證執行緒可?性
狀態資訊通過 protected 型別的 getState,setState,compareAndSetState 進?操作
//回傳同步狀態的當前值
protected final int getState() {
return state; }
// 設定同步狀態的值
protected final void setState(int newState) {
state = newState; }
//原?地(CAS操作)將同步狀態值設定為給定值update如果當前同步狀態的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
嘗試加鎖的時候通過CAS(CompareAndSwap)修改值,如果成功設定為1,并且把當前執行緒ID賦值,則代表加鎖成功,?旦獲取到鎖,其他的執行緒將會被阻塞進?阻塞佇列?旋,獲得鎖的執行緒釋放鎖的時候將會喚醒阻塞佇列中的執行緒,釋放鎖的時候則會把state重新置為0,同時當前執行緒ID置為空,

面試官:能說一下Semaphore/CountDownLatch/CyclicBarrier嗎
- Semaphore(信號量)-允許多個執行緒同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源,Semaphore(信號量)可以指定多個執行緒同時訪問某個資源,
- CountDownLatch(倒計時器): CountDownLatch是一個同步工具類,用來協調多個執行緒之間的同步,這個工具通常用來控制執行緒等待,它可以讓某一個執行緒等待直到倒計時結束,再開始執行,
- CyclicBarrier(回圈柵欄): CyclicBarrier 和 CountDownLatch 非常類似,它也可以實作執行緒間的技術等待,但是它的功能比 CountDownLatch 更加復雜和強大,主要應用場景和 CountDownLatch 類似,CyclicBarrier 的字面意思是可回圈使用(Cyclic)的屏障(Barrier),它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續干活,CyclicBarrier默認的構造方法是 CyclicBarrier(int parties),其引數表示屏障攔截的執行緒數量,每個執行緒呼叫await()方法告訴 CyclicBarrier 我已經到達了屏障,然后當前執行緒被阻塞,
volatile原理知道嗎?
相?synchronized的加鎖?式來解決共享變數的記憶體可?性問題,volatile就是更輕量的選擇,他沒有上下?切換的額外開銷成本,使?volatile宣告的變數,可以確保值被更新的時候對其他執行緒?刻可?,
volatile使?記憶體屏障來保證不會發?指令重排,解決了記憶體可?性的問題,
我們知道,執行緒都是從主記憶體中讀取共享變數到?作記憶體來操作,完成之后再把結果寫會主記憶體,但是這樣就會帶來可?性問題,舉個例?,假設現在我們是兩級快取的雙核CPU架構,包含L1、L2兩級快取,
那么,如果X變數?volatile修飾的話,當執行緒A再次讀取變數X的話,CPU就會根據快取?致性協議強制執行緒A重新從主記憶體加載最新的值到??的?作記憶體,?不是直接?快取中的值,
再來說記憶體屏障的問題,volatile修飾之后會加?不同的記憶體屏障來保證可?性的問題能正確執?,這?寫的屏障基于書中提供的內容,但是實際上由于CPU架構不同,重排序的策略不同,提供的記憶體屏障也不?樣,?如x86平臺上,只有StoreLoad?種記憶體屏障,
-
StoreStore屏障,保證上?的普通寫不和volatile寫發?重排序
-
StoreLoad屏障,保證volatile寫與后?可能的volatile讀寫不發?重排序
-
LoadLoad屏障,禁?volatile讀與后?的普通讀重排序
-
LoadStore屏障,禁?volatile讀和后?的普通寫重排序
面試官:說說你對Java記憶體模型(JMM)的理解,為什么要用JMM
本身隨著CPU和記憶體的發展速度差異的問題,導致CPU的速度遠快于記憶體,所以現在的CPU加?了?速快取,?速快取?般可以分為L1、L2、L3三級快取,基于上?的例?我們知道了這導致了快取?致性的問題,所以加?了快取?致性協議,同時導致了記憶體可?性的問題,?編譯器和CPU的重排序導致了原?性和有序性的問題,JMM記憶體模型正是對多執行緒操作下的?系列規范約束,通過JMM我們才屏蔽了不同硬體和作業系統記憶體的訪問差異,這樣保證了Java程式在不同的平臺下達到?致的記憶體訪問效果,同時也是保證在?效并發的時候程式能夠正確執?,

面試官:看你用到了執行緒池,能說說為什么嗎
- 提高執行緒的利用率,降低資源的消耗,
- 提高回應速度,執行緒的創建時間為T1,執行時間T2,銷毀時間T3,用執行緒池可以免去T1和T3的時間,
- 便于統一管理執行緒物件
- 可控制最大并發數
面試官:能說一下執行緒池的核心引數嗎?
來看一ThreadPoolExecutor的構造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
核?執行緒數corePoolSize :此值是用來初始化執行緒池中核心執行緒數,當執行緒池中執行緒池數<
corePoolSize時,系統默認是添加一個任務才創建一個執行緒池,可以通過呼叫prestartAllCoreThreads方法一次性的啟動corePoolSize個數的執行緒,當執行緒數 = corePoolSize時,新任務會追加到workQueue中, -
允許的最大執行緒數maximumPoolSize:
maximumPoolSize表示允許的最大執行緒數 = (非核心執行緒數+核心執行緒數),當BlockingQueue也滿了,但執行緒池中總執行緒數 <maximumPoolSize時候就會再次創建新的執行緒, -
活躍時間keepAliveTime:非核心執行緒 =(maximumPoolSize - corePoolSize ) ,非核心執行緒閑置下來不干活最多存活時間,
-
保持存活時間unit:執行緒池中非核心執行緒保持存活的時間
-
等待佇列workQueue:執行緒池 等待佇列,維護著等待執行的
Runnable物件,當運行當執行緒數= corePoolSize時,新的任務會被添加到workQueue中,如果workQueue也滿了則嘗試用非核心執行緒執行任務 -
執行緒工廠 threadFactory:創建一個新執行緒時使用的工廠,可以用來設定執行緒名、是否為daemon執行緒等等,
-
拒絕策略RejectedExecutionHandler:
corePoolSize、workQueue、maximumPoolSize都不可用的時候執行的 飽和策略,
面試官:完整說一下執行緒池的作業流程
-
執行緒池剛創建時,里面沒有一個執行緒,任務佇列是作為引數傳進來的,不過,就算佇列里面有任務,執行緒池也不會馬上執行它們,
-
當呼叫 execute() 方法添加一個任務時,執行緒池會做如下判斷:
-
a) 如果正在運行的執行緒數量小于 corePoolSize,那么馬上創建執行緒運行這個任務;
-
b) 如果正在運行的執行緒數量大于或等于 corePoolSize,那么將這個任務放入佇列;
-
c) 如果這時候佇列滿了,而且正在運行的執行緒數量小于 maximumPoolSize,那么還是要創建非核心執行緒立刻運行這個任務;
-
d) 如果佇列滿了,而且正在運行的執行緒數量大于或等于 maximumPoolSize,那么執行緒池會根據拒絕策略來對應處理,
-
當一個執行緒完成任務時,它會從佇列中取下一個任務來執行,
-
當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,如果當前運行的執行緒數大于 corePoolSize,那么這個執行緒就被停掉,所以執行緒池的所有任務完成后,它最侄訓收縮到 corePoolSize 的大小,
面試官:拒絕策略有哪些
主要有4種拒絕策略:
-
AbortPolicy:直接丟棄任務,拋出例外,這是默認策略
-
CallerRunsPolicy:只?調?者所在的執行緒來處理任務
-
DiscardOldestPolicy:丟棄等待佇列中最舊的任務,并執?當前任務
-
DiscardPolicy:直接丟棄任務,也不拋出例外
面試官:說一下你的核心執行緒數是怎么選的
執行緒在Java中屬于稀缺資源,執行緒池不是越大越好也不是越小越好,任務分為計算密集型、IO密集型、混合型,
- 計算密集型一般推薦執行緒池不要過大,一般是CPU數 + 1,+1是因為可能存在頁缺失(就是可能存在有些資料在硬碟中需要多來一個執行緒將資料讀入記憶體),如果執行緒池數太大,可能會頻繁的 進行執行緒背景關系切換跟任務調度,獲得當前CPU核心數代碼如下:
Runtime.getRuntime().availableProcessors();
- IO密集型:執行緒數適當大一點,機器的Cpu核心數*2,
- 混合型:如果密集型站大頭則拆分的必要性不大,如果IO型占據不少有必要,Mark 下,
面試官:說一下有哪些常見阻塞佇列
-
ArrayBlockingQueue :由陣列結構組成的有界阻塞佇列,
-
LinkedBlockingQueue :由鏈表結構組成的有界阻塞佇列,
-
PriorityBlockingQueue :支持優先級排序的無界阻塞佇列,
-
DelayQueue:使用優先級佇列實作的無界阻塞佇列,
-
SynchronousQueue:不存盤元素的阻塞佇列,
-
LinkedTransferQueue:由鏈表結構組成的無界阻塞佇列,
-
LinkedBlockingDeque:由鏈表結構組成的雙向阻塞佇列
面試官:說一下有哪幾種常見的執行緒池吧
在上面我們直接用到了ThreadPoolExecutor的構造方法創建執行緒池,還有另一種方式,通過Executors 創建執行緒,
需要注意的是,阿里巴巴Java開發手冊強制禁止使用Executors創建執行緒

比較典型常見的四種執行緒池包括:newFixedThreadPool、 newSingleThreadExecutor 、 newCachedThreadPool、
newScheduledThreadPool,
FixedThreadPool
-
定長的執行緒池,有核心執行緒,核心執行緒的即為最大的執行緒數量,沒有非核心執行緒,
-
使用的無界的等待佇列是
LinkedBlockingQueue,使用時候有堵滿等待佇列的風險,

SingleThreadPool
只有一條執行緒來執行任務,適用于有順序的任務的應用場景,也是用的無界等待佇列

CachedThreadPool
可快取的執行緒池,該執行緒池中沒有核心執行緒,非核心執行緒的數量為Integer.max_value,就是無限大,當有需要時創建執行緒來執行任務,沒有需要時回收執行緒,適用于耗時少,任務量大的情況,任務佇列用的是SynchronousQueue如果生產多快消費慢,則會導致創建很多執行緒需注意,

ScheduledThreadPoolExecutor
周期性執行任務的執行緒池,按照某種特定的計劃執行執行緒中的任務,有核心執行緒,但也有非核心執行緒,非核心執行緒的大小也為無限大,適用于執行周期性的任務,
看建構式:呼叫的還是ThreadPoolExecutor建構式,區別不同點在于任務佇列是用的DelayedWorkQueue,

- 面試官:這些題都能回答出來,很好,小伙子,很有精神!
- 老三:謝謝,那面試官老師,你看這一輪面試……
- 面試官:雖然你答的很好,但你的專案資料量只有十萬級,不符合我們的要求,所以,面試不能讓你過,
老三上去就是一個左刺拳,再接一個右正蹬……
- 面試官:啊……年輕人不講武德,來偷襲……
代碼地址:https://gitee.com/fighter3/thread-demo.git
好了,通過本文,相信你對多執行緒的應用和原理都有了一定的了解,文章開頭提到的crud仔就是博主本人了,技術水平有限,難免錯漏,歡迎指出,謝謝!
參考:
【1】:使用多執行緒查詢百萬條用戶資料將漢字轉化成拼音
【2】:講真 這次絕對讓你輕松學習執行緒池
【3】:SpringBoot學習筆記(十七:異步呼叫)
【4】:JavaGuide編著《JavaGuide面試突擊版》
【5】:艾小仙編著 《我想進大廠面試總結》
【6】:佚名編著 《Java核心知識點整理》
【7】:Java并發基礎知識,我用思維導圖整理好了
【8】:并發編程的鎖機制:synchronized和lock
【9】:詳解synchronized與Lock的區別與使用
【10】:bugstack小傅哥編著《Java面經手冊》
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/267089.html
標籤:其他
