1. 什么是行程、執行緒、協程,他們之間的關系是怎樣的?
- 行程:
- 本質上是一個獨立執行的程式,行程是作業系統進行資源分配和調度的基本概念,作業系統進行資源分配和調度的一個獨立單位,
- 執行緒:
- 是作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一個行程中可以并發多個執行緒,每條執行緒執行不同的任務,切換受系統控制,
- 協程:
- 又稱為微執行緒,是一種用戶態的輕量級執行緒,協程不像執行緒和行程需要進行系統內核上的背景關系切換,協程的背景關系切換是由用戶自己決定的,有自己的背景關系,所以說是輕量級的執行緒,也稱之為用戶級別的執行緒就叫協程,一個執行緒可以多個協程,執行緒行程都是同步機制,而協程則是異步 ,Java的原生語法中并沒有實作協程,目前python、Lua和GO等語言支持
- 關系:
- 一個行程可以有多個執行緒,它允許計算機同時運行兩個或多個程式,執行緒是行程的最小執行單位,CPU的調度切換的是行程和執行緒,行程和執行緒多了之后調度會消耗大量的CPU,CPU上真正運行的是執行緒,執行緒可以對應多個協程:

2. 說下并發和并行的區別,并舉例說明
- 并發 concurrency:
- 一核CPU,模擬出來多條執行緒,快速交替執行,
- 并行 parallellism:
- 多核CPU ,多個執行緒可以同時執行;
- eg: 執行緒池!
- 并發指在一段時間內宏觀上去處理多個任務,并行指同一個時刻,多個任務確實真的同時運行,
舉例:
#### 并發:是一心多用,聽課和看電影,但是CPU大腦只有一個,所以輪著來
#### 并行:火影忍者中的影分身,有多個你出現,可以分別做不同的事情
3. java實作多執行緒有哪幾種方式,有什么不同,比較常用哪種?
3.1 繼承Thread
- 繼承Thread,重寫里面
run()方法,創建實體,執行start - 優點:代碼撰寫最簡單直接操作
- 缺點:沒回傳值,繼承一個類后,沒法繼承其他的類,拓展性差
public class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("繼承Thread實作多執行緒,名稱:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.setName("demo1");
// 執行start
threadDemo1.start();
System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}
3.2 實作Runnable介面
- 自定義類實作Runnable,實作里面
run()方法,創建Thread類,使用Runnable介面的實作物件作為引數傳遞給Thread物件,呼叫Strat方法, - 優點:執行緒類可以實作多個幾介面,可以再繼承一個類
- 缺點:沒回傳值,不能直接啟動,需要通過構造一個Thread實體傳遞進去啟動
public class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("通過Runnable實作多執行緒,名稱:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
thread.setName("demo2");
// start執行緒執行
thread.start();
System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}
// JDK8之后采用lambda運算式
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("通過Runnable實作多執行緒,名稱:"+Thread.currentThread().getName());
});
thread.setName("demo2");
// start執行緒執行
thread.start();
System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}
3.3 實作Callable介面
- 創建callable介面的實作類,并實作
call()方法,結合FutureTask類包裝Callable物件,實作多執行緒, - 優點:有回傳值,拓展性也高
- 缺點:jdk5以后才支持,需要重寫
call()方法,結合多個類比如FutureTask和Thread類
public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("通過Callable實作多執行緒,名稱:"+Thread.currentThread().getName());
return "這是回傳值";
}
}
public static void main(String[] args) {
// JDK1.8 lambda運算式
FutureTask<Object> futureTask = new FutureTask<>(() -> {
System.out.println("通過Callable實作多執行緒,名稱:" +
Thread.currentThread().getName());
return "這是回傳值";
});
// MyTask myTask = new MyTask();
// FutureTask<Object> futureTask = new FutureTask<>(myTask);
// FutureTask繼承了Runnable,可以放在Thread中啟動執行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
// start執行緒執行
thread.start();
System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
try {
// 獲取回傳值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
// 阻塞等待中被中斷,則拋出
e.printStackTrace();
} catch (ExecutionException e) {
// 執行程序發送例外被拋出
e.printStackTrace();
}
}
3.4 通過執行緒池創建執行緒
- 自定義Runnable介面,實作run方法,創建執行緒池,呼叫執行方法并傳入物件
- 優點:安全高性能,復用執行緒
- 缺點: jdk5后才支持,需要結合Runnable進行使用
public class ThreadDemo4 implements Runnable {
@Override
public void run() {
System.out.println("通過執行緒池+runnable實作多執行緒,名稱:" +
Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 創建執行緒池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
// 執行緒池執行執行緒任務
executorService.execute(new ThreadDemo4());
}
System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
// 關閉執行緒池
executorService.shutdown();
}
- 一般常用的Runnable 和 第四種執行緒池+Runnable,簡單方便擴展,和高性能 (池化的思想)
3.5 Runable Callable Thread 三者區別?
- Thread是一個抽象類,只能被繼承,而Runable Callable是介面,需要實作介面中的方法
- 繼承Thread重寫
run()方法,實作Runable介面需要實作run()方法,而Callable是需要實作call()方法 - Thread和Runable 沒有回傳值,Callable 有回傳值
- 實作Runable 介面的類不能直接呼叫
start()方法,需要new 一個Thread并發該實作類放入Thread,再通過新建的Thread實體來呼叫start()方法, - 實作Callable 介面的類需要借助FutureTask(將該實作類放入其中),再將FutureTask實體放入Thread,再通過新建的Thread實體來呼叫
start()方法,獲取回傳值只需要借助FutureTask實體呼叫get()方法即可!
4. 執行緒的幾個狀態(生命周期)?
執行緒有幾個狀態(6個)!
public enum State {
/**
* 執行緒新生狀態
*/
NEW,
/**
* 執行緒運行中
*/
RUNNABLE,
/**
* 執行緒阻塞狀態
*/
BLOCKED,
/**
* 執行緒等待狀態,死等
*/
WAITING,
/**
* 執行緒超時等待狀態,超過一定時間就不再等
*/
TIMED_WAITING,
/**
* 執行緒終止狀態,代表執行緒執行完畢
*/
TERMINATED;
}
5. 執行緒狀態轉換的相關方法:sleep/yield/join wait/notify/notifyAll
Tread下的方法
##### sleep()
屬于執行緒Thread的方法,讓執行緒暫緩執行,等待預計時間之后再恢復
交出CPU使用權,《不會釋放鎖》,抱著鎖睡覺!
進入超時等待狀態TIME_WAITGING,睡眠結束變為就緒Runnable
##### yield()
屬于執行緒Thread的方法,暫停當前執行緒的物件,去執行其他執行緒
交出CPU使用權,《不會釋放鎖》,和sleep類似
作用:讓相同優先級的執行緒輪流執行,但是不保證一定輪流
注意:不會讓執行緒進入阻塞狀態BLOCKED,直接變為就緒Runnable,只需要重新獲得CPU使用權
##### join()
屬于執行緒Thread的方法,在主執行緒上運行呼叫該方法,會讓主執行緒休眠,
《不會釋放鎖》 讓呼叫join方法的執行緒先執行完畢,再執行其他執行緒
類似讓救護車警車優先通過!!
Object下的方法
##### wait()
屬于Object的方法,當前執行緒呼叫物件的wait方法,
《會釋放鎖》,進入執行緒的等待佇列
需要依靠notify或者notifyAll喚醒,或者wait(timeout)時間自動喚醒
##### notify()
屬于Object的方法
喚醒在物件監視器上等待的單個執行緒,《隨機喚醒》
##### notifyAll()
屬于Object的方法
喚醒在物件監視器上等待的全部執行緒,《全部喚醒》
執行緒狀態轉換流程圖

6. Java中可以有哪些方法來保證執行緒安全?
- 加鎖:比如synchronize/ReentrantLock
- 使用volatile宣告變數,輕量級同步,不能保證原子性(需要解釋)
- 使用執行緒安全類,例如原子類 AtomicXXX等
- 使用執行緒安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHashMap等
- ThreadLocal本地私有變數/信號量Semaphore等
7. 是否了解volatile關鍵字?能否解釋下它和synchronized有什么區別?
執行緒安全行:
執行緒安全性包括兩個方面,①可見性,②原子性!
volatile特性
-
參考文章: volatile關鍵字
-
volatile保證執行緒可見性案例:使用Volatile關鍵字的案例分析
-
原始碼分析文章參考:java同步系列之volatile決議
通俗來說就是,執行緒A對一個volatile變數的修改,對于其它執行緒來說是可見的,即執行緒每次獲取volatile變數的值都是最新的,
二者對比
- volatile是輕量級的synchronized,保證了共享變數的可見性,被volatile關鍵字修飾的變數,如果值發生了變化,其他執行緒立刻可見,避免出現臟讀現象!
- volatile輕量級,只能修飾變數,synchronized重量級,還可修飾方法
- volatile只能保證資料的可見性,不能用來同步,因為多個執行緒并發訪問volatile修飾的變數不會阻塞,
synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的執行緒才能進入臨界區,從而保證臨界區中的所有陳述句都全部執行,多個執行緒爭搶synchronized鎖物件時,會出現阻塞, - volatile:保證可見性,但是不能保證原子性
- synchronized:保證可見性,也保證原子性
使用場景
對變數的寫操作不依賴當前值,如多執行緒下執行a++,是無法通過volatile保證結果原子性的;
例:volatile int i = 0;并且大量執行緒呼叫i的自增操作,那么volatile可以保證變數的安全嗎?
不可以保證!,volatile不能保證變數操作的原子性!
-
自增操作包括三個步驟,分別是:讀取,加一,寫入,由于這三個子操作的原子性不能被保證,那么n個執行緒總共呼叫n次
i++的操作后,最后的i的值并不是大家想的n,而是一個比n小的數! -
解釋:
- 比如A執行緒執行自增操作,剛讀取到
i的初始值0,然后就被阻塞了! - B執行緒現在開始執行,還是讀取到
i的初始值0,執行自增操作,此時i的值為1 - 然后A執行緒阻塞結束,對剛才拿到的
0執行加1與寫入操作,執行成功后,i的值被寫成1了! - 我們預期輸出
2,可是輸出的是1,輸出比預期小!
- 比如A執行緒執行自增操作,剛讀取到
-
代碼實體:
public class VolatileTest { public volatile int i = 0; public void increase() { i++; } public static void main(String args[]) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); VolatileTest test = new VolatileTest(); for (int j = 0; j < 10000; j++) { Thread thread = new Thread(new Runnable() { @Override public void run() { test.increase(); } }); thread.start(); threadList.add(thread); } // 等待所有執行緒執行完畢 for (Thread thread : threadList) { thread.join(); } System.out.print(test.i);// 輸出9995 } }總結
volatile不需要加鎖,因此不會造成執行緒的阻塞,而且比synchronized更輕量級,而synchronized可能導致執行緒的阻塞!volatile由于禁止了指令重排,所以JVM相關的優化沒了,效率會偏弱!
##### JAVA記憶體模型簡稱 JMM
JMM規定所有的變數存在在主記憶體,每個執行緒有自己的作業記憶體,執行緒對變數的操作都在作業記憶體中進行,不能直接對主記憶體就行操作,
使用volatile修飾變數,每次讀取前必須從主記憶體屬性最新的值,每次寫入需要立刻寫到主記憶體中,volatile關鍵字修修飾的變數隨時看到的自己的最新值,假如執行緒1對變數v進行修改,那么執行緒2是可以馬上看見!

8. volatile可以避免指令重排,能否解釋下什么是指令重排?
- 指令重排序分兩類:
- 編譯器重排序
- 運行時重排序
JVM在編譯java代碼或者CPU執行JVM位元組碼時,對現有的指令進行重新排序,主要目的是為了優化運行效率(不改變程式結果的前提)
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定義順序: 1,2,3,4
計算順序: 1,3,2,4 和 2,1,3,4 結果都是一樣的
- 雖然指令重排序可以提高執行效率,但是多執行緒上可能會影響結果,有什么解決辦法?
- 解決辦法:記憶體屏障(了解即可~)
- 記憶體屏障是屏障指令,使CPU對屏障指令之前和之后的記憶體操作執行結果的一種約束!
擴展:現行發生原則happens-before(了解即可~)
volatile的記憶體可見性就體現了先行發生原則!
9. 介紹一下并發編程三要素?
- 原子性
- 有序性
- 可見性
9.1 原子性
- 原子性:
- 一個不可再被分割的最小顆粒,原子性指的是一個或多個操作要么全部執行成功要么全部執行失敗,期間不能被中斷,也不存在背景關系切換,執行緒切換會帶來原子性的問題!
int num = 1; // 原子操作
num++; // 非原子操作,從主記憶體讀取num到執行緒作業記憶體,進行+1,再把num寫回到主記憶體,
// 除非用原子類:即,java.util.concurrent.atomic里的原子變數類
// 解決辦法是可以用synchronized 或 Lock(比如ReentrantLock) 來把這個多步操作“變成”原子操作
// 這里不能使用volatile,前面有說到:對變數的寫操作不依賴當前值,如多執行緒下執行a++,是無法通過volatile保證結果原子性的
public class XdTest {
// 方式1:使用原子類
// AtomicInteger num = 0;// 這種方式的話++操作就可以保證原子性了,而不需要再加鎖了
private int num = 0;
// 方式2:使用lock,每個物件都是有鎖,只有獲得這個鎖才可以進行對應的操作
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}
// 方式3:使用synchronized,和上述是一個操作,這個是保證方法被鎖住而已,上述的是代碼塊被鎖住
public synchronized void add2(){
num++;
}
}
解決核心思想:把一個方法或者代碼塊看做一個整體,保證是一個不可分割的整體!
9.2 有序性
- 有序性:
- 程式執行的順序按照代碼的先后順序執行,因為處理器可能會對指令進行重排序JVM在編譯java代碼或者CPU執行JVM位元組碼時,對現有的指令進行重新排序,主要目的是優化運行效率(不改變程式結果的前提)
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定義順序: 1,2,3,4
計算順序: 1,3,2,4 和 2,1,3,4 結果都是一樣的(單執行緒情況下)
指令重排序可以提高執行效率,但是多執行緒上可能會影響結果!
假如下面的場景:
// 執行緒1
before();// 處理初始化作業,處理完成后才可以正式運行下面的run方法
flag = true; // 標記資源處理好了,如果資源沒處理好,此時程式就可能出現問題
// 執行緒2
while(flag){
run(); // 執行核心業務代碼
}
// -----------------指令重排序后,導致順序換了,程式出現問題,且難排查-----------------
// 執行緒1
flag = true; // 標記資源處理好了,如果資源沒處理好,此時程式就可能出現問題
// 執行緒2
while(flag){
run(); // 執行核心業務代碼
}
before();// 處理初始化作業,處理完成后才可以正式運行下面的run方法
9.3 可見性
- 可見性:
- 一個執行緒A對共享變數的修改,另一個執行緒B能夠立刻看到!
// 執行緒 A 執行
int num = 0;
// 執行緒 A 執行
num++;
// 執行緒 B 執行
System.out.print("num的值:" + num);
執行緒A執行 i++ 后再執行執行緒B,執行緒B可能有2個結果,可能是0和1,
因為i++ 在執行緒A中執行運算,并沒有立刻更新到主記憶體當中,而執行緒B就去主記憶體當中讀取并列印,此時列印的就是0;也可能執行緒A執行完成更新到主記憶體了,執行緒B的值是1,
所以需要保證執行緒的可見性:
synchronized、lock和volatile 能夠保證執行緒可見性
volatile保證執行緒可見性案例:使用Volatile關鍵字的案例分析
10. Java里面有哪些鎖?分別解釋下
樂觀鎖/悲觀鎖
- 悲觀鎖:
- 當執行緒去操作資料的時候,總認為別的執行緒會去修改資料,所以它每次拿資料的時候總會上鎖,別的執行緒去拿資料的時候就會阻塞,比如synchronized
- 樂觀鎖:
- 每次去拿資料的時候都認為別人不會修改,更新的時候會判斷是別人是否回去更新資料,通過版本來判斷,如果資料被修改了就拒絕更新,比如CAS是樂觀鎖,但嚴格來說并不是鎖,通過原子性來保證資料的同步,比如說資料庫的樂觀鎖,通過版本控制來實作,CAS不會保證執行緒同步,樂觀的認為在資料更新期間沒有其他執行緒影響
- 小結:悲觀鎖適合寫操作多的場景,樂觀鎖適合讀操作多的場景,樂觀鎖的吞吐量會比悲觀鎖大!
公平鎖/非公平鎖
- 公平鎖:
- 指多個執行緒按照申請鎖的順序來獲取鎖,簡單來說 如果一個執行緒組里,能保證每個執行緒都能拿到鎖 比如ReentrantLock(底層是同步佇列FIFO: First Input First Output來實作)
- 非公平鎖:
- 獲取鎖的方式是隨機獲取的,保證不了每個執行緒都能拿到鎖,也就是存在有執行緒餓死,一直拿不到鎖,比如synchronized、ReentrantLock
- 小結:非公平鎖性能高于公平鎖,更能重復利用CPU的時間,ReentrantLock中可以通過構造方法指定是否為公平鎖,默認為非公平鎖!synchronized無法指定為公平鎖,一直都是非公平鎖,
可重入鎖/不可重入鎖
- 可重入鎖:
- 也叫遞回鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖,一個執行緒獲取鎖之后再嘗試獲取鎖時會自動獲取鎖,可重入鎖的優點是避免死鎖,
- 不可重入鎖:
- 若當前執行緒執行某個方法已經獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞
- 小結:可重入鎖能一定程度的避免死鎖 synchronized、ReentrantLock都是可重入鎖!
獨占鎖/共享鎖
-
獨享鎖,是指鎖一次只能被一個執行緒持有,
- 也叫X鎖/排它鎖/寫鎖/獨享鎖:該鎖每一次只能被一個執行緒所持有,加鎖后任何執行緒試圖再次加鎖的執行緒會被阻塞,直到當前執行緒解鎖,例子:如果 執行緒A 對 data1 加上排他鎖后,則其他執行緒不能再對 data1 加任何型別的鎖,獲得獨享鎖的執行緒即能讀資料又能修改資料!
-
共享鎖,是指鎖一次可以被多個執行緒持有,
- 也叫S鎖/讀鎖,能查看資料,但無法修改和洗掉資料的一種鎖,加鎖后其它用戶可以并發讀取、查詢資料,但不能修改,增加,洗掉資料,該鎖可被多個執行緒所持有,用于資源資料共享!
ReentrantLock和synchronized都是獨享鎖,ReadWriteLock的讀鎖是共享鎖,寫鎖是獨享鎖,
互斥鎖/讀寫鎖
與獨享鎖/共享鎖的概念差不多,是獨享鎖/共享鎖的具體實作,
ReentrantLock和synchronized都是互斥鎖,ReadWriteLock是讀寫鎖
自旋鎖
- 自旋鎖:
- 一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那么該執行緒將回圈等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出回圈,任何時刻最多只能有一個執行單元獲得鎖,
- 不會發生執行緒狀態的切換,一直處于用戶態,減少了執行緒背景關系切換的消耗,缺點是回圈會消耗CPU,
- 常見的自旋鎖:TicketLock,CLHLock,MSCLock
死鎖
- 死鎖:
- 兩個或兩個以上的執行緒在執行程序中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法讓程式進行下去!

下面三種是Jvm為了提高鎖的獲取與釋放效率而做的優化 針對Synchronized的鎖升級,鎖的狀態是通過物件監視器在物件頭中的欄位來表明,是不可逆的程序
- 偏向鎖:
- 一段同步代碼一直被一個執行緒所訪問,那么該執行緒會自動獲取鎖,獲取鎖的代價更低!
- 輕量級鎖:
- 當鎖是偏向鎖的時候,被其他執行緒訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,但不會阻塞,且性能會高點!
- 重量級鎖:
- 當鎖為輕量級鎖的時候,其他執行緒雖然是自旋,但自旋不會一直回圈下去,當自旋一定次數的時候且還沒有獲取到鎖,就會進入阻塞,該鎖升級為重量級鎖,重量級鎖會讓其他申請的執行緒進入阻塞,性能也會降低!
11. 寫個多執行緒死鎖的例子
執行緒在獲得了鎖A并且沒有釋放的情況下去申請鎖B,這時另一個執行緒已經獲得了鎖B,在釋放鎖B之前又要先獲得鎖A,因此倍訓發生,陷入死鎖回圈:
public class DeadLockDemo {
private static String locka = "locka";
private static String lockb = "lockb";
public void methodA(){
synchronized (locka){
System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
// 讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);// sleep不釋放鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockb){
System.out.println("我是A方法中獲得了鎖B "+Thread.currentThread().getName() );
}
}
}
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中獲得了鎖B "+Thread.currentThread().getName() );
// 讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);// sleep不釋放鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locka){
System.out.println("我是B方法中獲得了鎖A "+Thread.currentThread().getName() );
}
}
}
public static void main(String [] args){
System.out.println("主執行緒運行開始運行:"+Thread.currentThread().getName());
DeadLockDemo deadLockDemo = new DeadLockDemo();
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
System.out.println("主執行緒運行結束:"+Thread.currentThread().getName());
}
}
死鎖的4個必要條件:
- 互斥條件:資源不能共享,只能由一個執行緒使用!
- 請求與保持條件:執行緒已經獲得一些資源,但因請求其他資源發生阻塞,對已經獲得的資源保持不釋放!
- 不可搶占:有些資源是不可強占的,當某個執行緒獲得這個資源后,系統不能強行回收,只能由執行緒使用完自己釋放!
- 回圈等待條件:多個執行緒形成環形鏈,每個都占用對方申請的下個資源!
只要發生死鎖,上面的條件都成立,只要一個不滿足,就不會發生死鎖!
12. 設計一個簡單的不可重入鎖
不可重入鎖:若當前執行緒執行某個方法已經獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞!
public class UnreentrantLock {
private boolean isLocked = false;
// 加鎖方法
public synchronized void lock() throws InterruptedException {
System.out.println("進入lock加鎖 "+Thread.currentThread().getName());
// 判斷是否已經被鎖,如果被鎖則當前請求的執行緒進行等待
while (isLocked){
System.out.println("進入wait等待 "+Thread.currentThread().getName());
wait();
}
// 如果還沒被加鎖,則進行加鎖
isLocked = true;
}
// 解鎖方法
public synchronized void unlock(){
System.out.println("進入unlock解鎖 "+Thread.currentThread().getName());
isLocked = false;
// 喚醒物件鎖池里面的一個執行緒
notify();
}
}
public class Main {
private UnreentrantLock unreentrantLock = new UnreentrantLock();
// 加鎖建議在try里面,解鎖建議在finally
public void methodA(){
try {
unreentrantLock.lock();
System.out.println("methodA方法被呼叫");
// methodA()中嵌套呼叫methodB(),測驗methodB()是否能獲取鎖的執行權
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public void methodB(){
try {
unreentrantLock.lock();
System.out.println("methodB方法被呼叫");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public static void main(String [] args){
// 演示同一個執行緒下是否可沖入!(如果單執行緒都是不可重入的話,多執行緒下就不用說了~)
new Main().methodA();
}
}
// 同一個執行緒,重復獲取鎖失敗,形成死鎖,這個就是不可重入鎖

13. 設計一個簡單的可重入鎖
可重入鎖:也叫遞回鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖
public class ReentrantLock {
private boolean isLocked = false;
// 用于記錄是不是重入的執行緒
private Thread lockedOwner = null;
// 累計加鎖次數,加鎖一次累加1,解鎖一次減少1
private int lockedCount = 0;
// 加鎖方法
public synchronized void lock() throws InterruptedException {
System.out.println("進入lock加鎖 "+Thread.currentThread().getName());
// 獲取當前執行緒
Thread thread = Thread.currentThread();
// 判斷是否是同個執行緒獲取鎖, lockedOwner != thread參考地址的比較
// 如果已經加鎖,且當前執行緒不是之前加鎖的執行緒則阻塞等待!
while (isLocked && lockedOwner != thread ){
System.out.println("進入wait等待 "+Thread.currentThread().getName());
System.out.println("當前鎖狀態 isLocked = "+isLocked);
System.out.println("當前count數量 lockedCount = "+lockedCount);
wait();
}
// 如果沒有加鎖,或者當前執行緒是之前加鎖的執行緒,則:
// 進行加鎖,兩次執行緒地址相同,加鎖次數++
isLocked = true;
lockedOwner = thread;
lockedCount++;
}
// 解鎖方法
public synchronized void unlock(){
System.out.println("進入unlock解鎖 "+Thread.currentThread().getName());
// 獲取當前執行緒
Thread thread = Thread.currentThread();
// 執行緒A加的鎖,只能由執行緒A解鎖,其他執行緒B不能解鎖
if(thread == this.lockedOwner){
lockedCount--;
if(lockedCount == 0){
// 解鎖
isLocked = false;
lockedOwner = null;
// 喚醒物件鎖池里面的一個執行緒
notify();
}
}
}
}
public class Main {
//private UnreentrantLock unreentrantLock = new UnreentrantLock();
private ReentrantLock reentrantLock = new ReentrantLock();
// 加鎖建議在try里面,解鎖建議在finally
public void methodA(){
try {
reentrantLock.lock();
System.out.println("methodA方法被呼叫");
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void methodB(){
try {
reentrantLock.lock();
System.out.println("methodB方法被呼叫");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String [] args){
for(int i=0 ;i<10;i++){
// 演示的是同個執行緒
new Main().methodA();
}
}
}
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ttX91EiY-1613882316860)(小滴課堂并發與多執行緒相關面試題總結.assets/image-20210220161315283.png)]
14. 介紹下你對synchronized的理解?
原始碼分析文章參考:java同步系列之synchronized決議
- synchronized是解決執行緒安全的問題,常用在同步普通方法、靜態方法、代碼塊中使用!
- synchronized非公平、可重入鎖!
- 每個物件有一個鎖和一個等待佇列,鎖只能被一個執行緒持有,其他需要鎖的執行緒需要阻塞等待,鎖被釋放后,物件會從佇列中取出一個并喚醒,喚醒哪個執行緒是不確定的,不保證公平性
15. 解釋下什么是CAS?以及ABA問題?
CAS全稱:Compare and Swap 比較并交換
Unsafe實作原理,參考文章:java魔法類之Unsafe決議
- CAS底層通過Unsafe類實作原子性操作,操作包含三個運算元:
- 物件記憶體地址(V):
- 預期原值(A):
- 新值(B)
- 理解方式1:比較當前作業記憶體中的值和主記憶體中的值,如果這個值是期望的,那么則執行交換操作!如果不是就一直回圈!
- 理解方式2:如果記憶體地址中的值與預期原值相匹配,那么處理器會自動將該地址的值更新為新值 ,若果在第一輪回圈中,a執行緒獲取地址里面的值被b執行緒修改了,那么a執行緒需要自旋,到下次回圈才有可能機會執行,
CAS屬于樂觀鎖,性能較悲觀鎖有很大的提高!
AtomicXXX 等原子類底層就是CAS實作,一定程度比synchonized好,因為后者是悲觀鎖!

小滴老師講這塊的時候,對于第一次接觸CAS的萌新有些不好理解,這里我參考狂神老師在介紹CAS的時候的一些理解:以一個案例入手:
案例:
public class CASDemo {
// CAS compareAndSet : 比較并交換!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值達到了,那么就更新,否則,
// 就不更新, CAS 是CPU的并發原語!
System.out.println(atomicInteger.compareAndSet(2020, 2021));// true
System.out.println(atomicInteger.get());// 2021
//atomicInteger.getAndIncrement()// 看底層如何實作 ++
System.out.println(atomicInteger.compareAndSet(2020, 2021));// false
System.out.println(atomicInteger.get());// 2021
}
}
我們來看一下getAndIncrement()方法的底層實作:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// UnSafe類,底層是呼叫C++:Java無法操作記憶體,所以這里借助C++來操作記憶體
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 獲取記憶體偏移值valueOffset
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// value被volatile修飾,避免指令重排,且保證執行緒可見性和有序性
private volatile int value;
...
public final int getAndIncrement() {
// 引數:
// this: 當前物件
// valueOffset:當前物件的記憶體偏移地址
// 1:值
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
大致了解UnSafe后,我們繼續點進getAndIncrement()方法中,unsafe呼叫的getAndAddInt()方法查看:
// 位于UnSafe類中
// 引數:var1 當前物件,var2 當前物件的記憶體偏移地址,var4 值(1)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// 這里用到了自旋鎖:一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那么該執行緒將回圈等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出回圈,任何時刻最多只能有一個執行單元獲得鎖,
do {
// 獲取記憶體地址中的原物件的值
var5 = this.getIntVolatile(var1, var2);
// 借助CAS比較并交換,來實作getAndIncrement()方法的自增+1功能!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
// 呼叫C++,執行比較并交換
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS : 比較當前作業記憶體中的值和主記憶體中的值,如果這個值是期望的,那么則執行操作!如果不是就
一直回圈!
CAS的ABA問題?
貍貓換太子

public class CasAbaTest {
// CAS compareAndSet : 比較并交換!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
/*
* 類似于我們平時寫的SQL:樂觀鎖
*
* 如果某個執行緒在執行操作某個物件的時候,其他執行緒若操作了該物件,
* 即使物件內容未發生變化,也需要告訴我,
*
* 期望、更新:
* public final boolean compareAndSet(int expect, int update)
* 如果我期望的值達到了,那么就更新,否則,就不更新,
* CAS 是CPU的并發原語!
*/
// ============== 搗亂的執行緒 ==================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// ============== 期望的執行緒 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
輸出結果:
true
2021
true
2020
true
6666
上述案例中:假設我們期望的執行緒本來是需要將2020更換成6666,然而有一個搗亂的執行緒搶在期望執行緒之前執行,先把2020更換為了2021,然后又將2021更換回2020!
這樣看上去當期望執行緒執行時,初始值仍為2020沒有改變,但是實際上在搗亂執行緒中已經執行過2次更換操作了,而我們的期望執行緒并不知情!這就是ABA問題!
如何解決ABA問題?
本質上相當于采用樂觀鎖策略解決ABA問題!
public class CASDemo {
/**
* AtomicStampedReference 注意,
* 如果泛型是一個包裝類,就需要注意物件的參考問題
* 正常在業務操作,這里面比較的都是一個個物件
*/
// 引數1:初始值100
// 引數2:初始對應的版本號 initialStamp=1
static AtomicStampedReference<Integer> atomicStampedReference =
new AtomicStampedReference<>(100,1);
// CAS compareAndSet : 比較并交換!
public static void main(String[] args) {
// 執行緒A:
new Thread(()->{
// 執行緒執行時,先獲得initialStamp版本號
int stamp = atomicStampedReference.getStamp();
System.out.println("A執行緒第1次拿到的版本號為:"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// cas比較并交換:100--->101
atomicStampedReference.compareAndSet(
100,
101,
atomicStampedReference.getStamp(),// 獲得最新版本號
// 更新版本號
atomicStampedReference.getStamp() + 1);
System.out.println("A執行緒第2次拿到的版本號為:"
+atomicStampedReference.getStamp());
// cas比較并交換:101--->100
System.out.println("A執行緒第2次是否執行了CAS:" +
atomicStampedReference.compareAndSet(
101,
100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("A執行緒第3次拿到的版本號為:"
+atomicStampedReference.getStamp());
},"A").start();
// 樂觀鎖的原理相同!
// 執行緒B:
new Thread(()->{
// 獲得版本號
int stamp = atomicStampedReference.getStamp();
System.out.println("B執行緒第1次拿到的版本號為:"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// cas比較并交換:100--->99
System.out.println("B執行緒第1次是否執行了CAS:" +
atomicStampedReference.compareAndSet(
100,
99,
stamp,
stamp + 1));
System.out.println("B執行緒第2次拿到的版本號為:"
+atomicStampedReference.getStamp());
},"B").start();
}
}
這樣,在版本號initialStamp的限制下,每執行一次CAS,都會將版本號+1,這樣即使出現了 “貍貓換太子” 情況,期望執行緒也能及時知道!
輸出結果如下:
A執行緒第1次拿到的版本號為:1
B執行緒第1次拿到的版本號為:1
A執行緒第2次拿到的版本號為:2
A執行緒第2次是否執行了CAS:true
A執行緒第3次拿到的版本號為:3
B執行緒第1次是否執行了CAS:false
B執行緒第2次拿到的版本號為:3
總的來說,與MySQL的樂觀鎖表中加一個version欄位原理相同!
注意:
Integer 使用了物件快取機制,默認范圍是 -128 ~ 127 ,推薦使用靜態工廠方法 valueOf 獲取物件實體,而不是 new,因為 valueOf 使用快取,而 new 一定會創建新的物件分配新的記憶體空間;
下面是阿里巴巴開發手冊的規范點:

所以上面的案例,如果使用大于-128-127范圍的數字時候就會出現2個flase的情況!這里小伙伴一定要注意下~
16. 介紹下你對AQS的理解?
參考文章:AQS面試詳解
AQS的全稱為(AbstractQueuedSynchronizer)抽象的佇列式同步器,是除了java自帶的synchronized關鍵字之外的鎖機制,這個類在java.util.concurrent.locks包下面,
它是一個Java提高的底層同步工具類,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的!
實作了AQS的鎖有:自旋鎖、互斥鎖、讀鎖寫鎖、條件產量、信號量、柵欄都是AQS的衍生物!
17. ReentrantLock和synchronized的差別?
- ReentrantLock和synchronized都是獨占鎖,可重入鎖,悲觀鎖
- synchronized:
- 1、java內置關鍵字
- 2、無法判斷是否獲取鎖的狀態,只能是非公平鎖!
- 3、加鎖解鎖的程序是隱式的,用戶不用手動操作,優點是操作簡單但顯得不夠靈活
- 4、一般并發場景使用足夠、可以放在被遞回執行的方法上,且不用擔心執行緒最后能否正確釋放鎖
- ReentrantLock:
- 1、是個Lock介面的實作類
- 2、可以判斷是否獲取到鎖,可以為公平鎖也可以是非公平鎖(默認)
- 3、需要手動加鎖和解鎖,且解鎖的操作盡量要放在finally代碼塊中,保證執行緒正確釋放鎖
- 5、創建的時候通過傳進引數true創建公平鎖,如果傳入的是false或沒傳引數則創建的是非公平鎖
- 6、底層是AQS的state和FIFO佇列來控制加鎖
18. ReentrantReadWriteLock和ReentrantLock有什么區別?

ReentrantReadWriteLock
1、讀寫鎖介面ReadWriteLock介面的一個具體實作,實作了讀寫鎖分離
2、支持公平和非公平,底層也是基于AQS實作
3、允許從寫鎖降級為讀鎖:
流程:先獲取寫鎖,然后獲取讀鎖,最后釋放寫鎖;但不能從讀鎖升級到寫鎖
4、重入:
-
讀鎖后還可以獲取讀鎖;
-
獲取了寫鎖之后既可以再次獲取寫鎖又可以獲取讀鎖
-
讀鎖是共享的,寫鎖是獨占的!讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,主要是提升了讀寫的性能 !
ReentrantLock是獨占鎖且可重入的,相比synchronized而言功能更加豐富也更適合復雜的并發場景,但是也有弊端,假如有兩個執行緒A/B訪問資料,加鎖是為了防止執行緒A在寫資料, 執行緒B在讀資料造成的資料不一致; 但執行緒A在讀資料,執行緒C也在讀資料,讀資料是不會改變資料沒有必要加鎖,但是ReentrantLock還是加鎖了,降低了程式的性能,所以就有了ReadWriteLock讀寫鎖介面!
19. 是否了解阻塞佇列BlockingQueue?
BlockingQueue阻塞佇列
- ArrayBlockingQueue,ArrayBlockingQueue
- put方法用來向隊尾存入元素,如果佇列滿,則阻塞
- take方法用來從隊首取元素,如果佇列為空,則阻塞
BlockingQueue: juc包下的提供了執行緒安全的佇列訪問的介面,并發包下很多高級同步類的實作都是基于阻塞佇列實作的!
- 1、當阻塞佇列進行插入資料時,如果佇列已滿,執行緒將會阻塞等待直到佇列非滿
- 2、從阻塞佇列讀資料時,如果佇列為空,執行緒將會阻塞等待直到佇列里面是非空的時候
常見的阻塞佇列
- ArrayBlockingQueue:
- 基于陣列實作的一個阻塞佇列,需要指定容量大小,FIFO先進先出順序;
- LinkedBlockingQueue:
- 基于鏈表實作的一個阻塞佇列,如果不指定容量大小,默認
Integer.MAX_VALUE,FIFO先進先出順序;
- 基于鏈表實作的一個阻塞佇列,如果不指定容量大小,默認
- PriorityBlockingQueue:
- 一個支持優先級的無界阻塞佇列,默認情況下元素采用自然順序升序排序,也可以自定義排序實作java.lang.Comparable介面;
- DelayQueue:
- 延遲佇列,在指定時間才能獲取佇列元素的功能,佇列頭元素是最接近過期的元素,里面的物件必須實作 java.util.concurrent.Delayed 介面并實作CompareTo和getDelay方法;
?
擴展:你知道非阻塞佇列ConcurrentLinkedQueue嗎,它怎么實作執行緒安全的?
參考文章:Java并發編程之ConcurrentLinkedQueue詳解
20. java里有哪些是常用的執行緒池?
使用執行緒池的好處:
重用存在的執行緒,減少物件創建銷毀的開銷,有效的控制最大并發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞,且可以定時定期執行、單執行緒、并發數控制,配置任務過多任務后的拒絕策略等功能
類別:
- newFixedThreadPool :
- 一個定長執行緒池,可控制執行緒最大并發數
- newCachedThreadPool:
- 一個可快取執行緒池
- newSingleThreadExecutor:
- 一個單執行緒化的執行緒池,用唯一的作業執行緒來執行任務
- newScheduledThreadPool:
- 一個定長執行緒池,支持定時/周期性任務執行
【阿里巴巴編碼規范】 執行緒池不允許使用 Executors 去創建,要通過 ThreadPoolExecutor的方式原因?
Executors創建的執行緒池底層也是呼叫 ThreadPoolExecutor,只不過使用不同的引數、佇列、拒絕策略等
如果使用不當,會造成資源耗盡問題
直接使用ThreadPoolExecutor讓使用者更加清楚執行緒池允許規則,常見引數的使用,避免風險
##### 常見的執行緒池問題:
newFixedThreadPool和newSingleThreadExecutor:
佇列使用LinkedBlockingQueue,佇列長度為 Integer.MAX_VALUE,可能造成堆積,導致OOM
newScheduledThreadPool和newCachedThreadPool:
執行緒池里面允許最大的執行緒數是Integer.MAX_VALUE,可能會創建過多執行緒,導致OOM
ThreadPoolExecutor建構式里面的引數,能否解釋下各個引數的作用?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:核心執行緒數,執行緒池也會維護執行緒的最少數量,默認情況下核心執行緒會一直存活,即使沒有任務也不會受存keepAliveTime控制!
坑:在剛創建執行緒池時執行緒不會立即啟動,到有任務提交時才開始創建執行緒并逐步執行緒數目達到corePoolSize -
maximumPoolSize:執行緒池維護執行緒的最大數量,超過將被阻塞!
坑:當核心執行緒滿,且阻塞佇列也滿時,才會判斷當前執行緒數是否小于最大執行緒數,才決定是否創建新執行緒 -
keepAliveTime:非核心執行緒的閑置超時時間,超過這個時間就會被回收,直到執行緒數量等于corePoolSize -
unit:指定keepAliveTime的單位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS -
workQueue:執行緒池中的任務佇列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue -
threadFactory:創建新執行緒時使用的工廠 -
handler:RejectedExecutionHandler是一個介面且只有一個方法,執行緒池中的數量大于maximumPoolSize,對拒絕任務的處理策略,默認有4種策略:- AbortPolicy
- CallerRunsPolicy
- DiscardOldestPolicy
- DiscardPolicy
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262189.html
標籤:java
上一篇:遞增順序二叉查找樹(樹的中序遍歷)Leetcode 刷題日記 2021.2.20
下一篇:JVM類的加載機制
