并發編程面試題
- 第一關: 初出茅廬
- 1.什么是行程?
- 2.什么是執行緒?
- 3.執行緒的實作方式?
- 4.執行緒的狀態?
- 5.run方法和start方法的區別
- 6.獲取當前執行緒的名字?
- 7.判斷執行緒是否存活?
- 8.sleep()方法的作用?
- 9.執行緒的種類
- 10.什么是synchronized?
- 11.執行緒的基本方法
- 12.為什么 wait 和 notify 方法要在同步塊中呼叫?
- 13.怎么檢測一個執行緒是否擁有鎖?
- 14.volatile 變數和 atomic 變數有什么不同?
- 15.為什么執行緒通信的方法 wait(), notify()和 notifyAll()被定義在 Object 類里?
- 16.為什么 Thread 類的 sleep()和 yield ()方法是靜態的?
- 17.并發編程三要素?
- 18.Executors 類是什么?
- 第二關:小試牛刀
- 1.如何優雅的設定睡眠時間
- 2.如何停止一個執行緒
- 3.yield()方法和join()的作用
- 4.執行緒的優先級
- 5.interrupted方法和isInterrupted方法的區別?
- 6.Java虛擬機退出時Daemon執行緒中的finally塊一定會執行嗎?
- 7.設定執行緒背景關系類加載器
- 8.并發和并行
- 9.什么是多執行緒中的背景關系切換?
- 10.死鎖與活鎖的區別,死鎖與饑餓的區別?
- 11.Java 中用到的執行緒調度演算法是什么?
- 12.什么是執行緒組,為什么在 Java 中不推薦使用?
- 13.sleep 與 wait 的區別
- 14.Java后臺執行緒
- 15.死鎖案例和死鎖分析
- 16.ThreadLocal 作用(執行緒本地存盤)
- 17.Java 中用到的執行緒調度
- 18.一個執行緒運行時發生例外會怎樣?
- 19.同步方法和同步塊,哪個是更好的選擇?
- 20.多執行緒的價值?
- 21. 創建執行緒的三種方式的對比?
- 22. Java 執行緒數過多會造成什么后果?
- 23.執行緒的調度策略
- 24.什么是 Future?
- 25.寫一個Callable案例
- 第三關:過關斬將
- 1.synchronized底層的兩個jvm指令
- 2.java物件頭
- 3.鎖的升降級規則
- 4.偏向鎖
- 5.輕量級鎖
- 6.重量級鎖
- 7.鎖優化
- 8.鎖對比
- 9.生產者消費者模型
- 10.synchronized和lock的區別lock有什么好處?
- 11.樂觀鎖
- 12.悲觀鎖
- 13.自旋鎖
- 14.可重入鎖(遞回鎖)
- 15.公平鎖與非公平鎖
- 16.ReadWriteLock 讀寫鎖
- 17.共享鎖和獨占鎖
- 18.分段鎖
- 19.tryLock 和 lock 和 lockInterruptibly 的區別
- 20.什么是作業竊取演算法?
- 21.Java中的原子更新類
- 22.SynchronizedMap 和 ConcurrentHashMap 有什么區別?
- 23.多執行緒與單例模式
- 24.什么是 Java Timer 類?如何創建一個有特定時間間隔的任務?
- 第四關:登峰造極
- 1.什么是CAS
- 2.UnSafe類(jdk.internal.misc.UnSafe)
- 3.CAS原理分析
- 4.CAS出現的問題分析與解決
- 5.JMM模型(java記憶體模型規范不是真實存在)
- 6.如何實作一個自旋鎖
- 7.volatile的作用
- 8.執行緒池相關問題
- 9.Synchronized 同步鎖分析
- 10.執行緒中斷(interrupt)
- 11.執行緒背景關系切換
- 12.JAVA 阻塞佇列
- 13.CyclicBarrier、CountDownLatch、Semaphore ,Exchanger的用法
- 14.LockSupport
- 15.Java并發容器有哪些?
- 16.什么是寫時復制
- 17.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
- 18.什么是 AQS(抽象的佇列同步器)
第一關: 初出茅廬
1.什么是行程?
行程是系統中正在運行的一個程式,程式一旦運行就是行程,
行程可以看成程式執行的一個實體,行程是系統資源分配的獨立物體,每個行程都擁有獨立的地址空間一個行程無法訪問另一個行程的變數和資料結構,如果想讓一個行程訪問另一個行程的資源,需要使用行程間通信,比如管道,檔案,套接字等,
2.什么是執行緒?
是作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以并發多個執行緒,每條執行緒并行執行不同的任務,
3.執行緒的實作方式?
1.繼承Thread類
2.實作runnable介面
3.實作callable介面
4.執行緒池
4.執行緒的狀態?
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW狀態:new創建一個Thread物件時, 并沒處于執行狀態,因為沒有呼叫start方法啟動改執行緒,那么此時的狀態就是新建狀態,
- RUNNABLE狀態:執行緒物件通過start方法進入runnable狀態,啟動的執行緒不一定會立即得到執行,執行緒的運行與否要看cpu的調度,我們把這個中間狀態叫可執行狀態(RUNNABLE),
- RUNNING狀態:一旦cpu通過輪詢或其他方式從任務可以執行佇列中選中了執行緒,此時它才能真正的執行自己的邏輯代碼,
- BLOCKED狀態:處于這種狀態的執行緒不會被分配CPU執行時間,它們要等待被顯式地喚醒,否則會處于無限期等待的狀態,
- TIMED_WAITING狀態:處于這種狀態的執行緒不會被分配CPU執行時間,不過無須無限期等待被其他執行緒顯示地喚醒,在達到一定時間后它們會自動喚醒,
- TERMINATED狀態:當執行緒的run()方法完成時,或者主執行緒的main()方法完成時,我們就認為它終止了,這個執行緒物件也許是活的,但是,它已經不是一個單獨執行的執行緒,執行緒一旦終止了,就不能復生,
5.run方法和start方法的區別
- start ()方法來啟動執行緒,真正實作了多執行緒運行,這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過呼叫Thread類的star()方法來啟動一個執行緒,這時此執行緒是處于就緒狀態,并沒有運行,然后通過此Thread類呼叫方法run()來完成其運行操作的,這里方法run()稱為執行緒體,它包含了要執行的這個執行緒的內容,Run方 法運行結束,此執行緒終止,然后CPU再調度其它執行緒,
- run () 方法當作普通方法的方式呼叫,程式還是要順序執行,要等待run方法體執行完畢后,才可繼續執行下面的代碼;程式中只有主執行緒一這 一個執行緒,其程式執行路徑還是只有一條,這樣就沒有達到多執行緒的目的,
6.獲取當前執行緒的名字?
System.out.println(Thread.currentThread().getName());
7.判斷執行緒是否存活?
執行緒.isAlive();
8.sleep()方法的作用?
方法sleep()的作用是在指定的毫秒數內讓當前的“正在執行的執行緒”休眠(暫停執行),
9.執行緒的種類
java中執行緒分為用戶執行緒和守護執行緒(GC就是一個守護執行緒)
守護執行緒的特點:守護執行緒是一個比較特殊的執行緒,主要被用做程式中后臺調度以及支持性作業,當Java虛擬機中不存在非守護執行緒時,守護執行緒才會隨著JVM一同結束作業,
//設定為守護執行緒
thread.setDaemon(true)
Daemon屬性需要再啟動執行緒之前設定,不能再啟動后設定,
10.什么是synchronized?
synchronized是java中的一個關鍵字可以用來修飾方法和變數來保證執行緒的同步,
普通同步方法一> 鎖的是當前實體物件,
靜態同步方法一>鎖的是當前類的Class物件,
同步方法塊一>鎖的是synchonized括號里配置的物件,
11.執行緒的基本方法
執行緒相關的基本方法有 wait,notify,notifyAll,sleep,join,yield 等,

| 方法名 | 功能 |
|---|---|
| sleep() | 強迫一個執行緒睡眠N毫秒, |
| isAlive() | 判斷一個執行緒是否存活, |
| join() | 等待執行緒終止, |
| activeCount() | 程式中活躍的執行緒數, |
| enumerate() | 列舉程式中的執行緒, |
| currentThread() | 得到當前執行緒, |
| isDaemon() | 一個執行緒是否為守護執行緒, |
| setDaemon() | 設定一個執行緒為守護執行緒,(用戶執行緒和守護執行緒的區別在于,是否等待主執行緒依賴于主執行緒結束而結束) |
| setName() | 為執行緒設定一個名稱, |
| wait() | 強迫一個執行緒等待, |
| notify() | 通知一個執行緒繼續運行, |
| setPriority() | 設定一個執行緒的優先級, |
| getPriority(): | 獲得一個執行緒的優先級, |
| yieid() | yield 會使當前執行緒讓出 CPU 執行時間片,與其他執行緒一起重新競爭 CPU 時間片, |
12.為什么 wait 和 notify 方法要在同步塊中呼叫?
Java API 強制要求這樣做,如果你不這么做,你的代碼會拋出
IllegalMonitorStateException 例外,還有一個原因是為了避免 wait 和 notify之間產生競態條件,
當一個執行緒需要呼叫物件的 wait()方法的時候,這個執行緒必須擁有該物件的鎖,接著它就會釋放這個物件鎖并進入等待狀態直到其他執行緒呼叫這個物件上的 notify()方法,同樣的,當一個執行緒需要呼叫物件的 notify()方法時,它會釋放這個物件的鎖,以便其他在等待的執行緒就可以得到這個物件鎖,由于所有的這些方法都需要執行緒持有物件的鎖,這樣就只能通過同步來實作,所以他們只能在同步方法或者同步塊中被呼叫,
13.怎么檢測一個執行緒是否擁有鎖?
在 java.lang.Thread 中有一個方法叫 holdsLock(),它回傳 true 如果當且僅當當前執行緒擁有某個具體物件的鎖,
14.volatile 變數和 atomic 變數有什么不同?
Volatile 變數可以確保先行關系,即寫操作會發生在后續的讀操作之前, 但它并不能保證原子性,例如用 volatile 修飾 count 變數那么 count++ 操作就不是原子性的,
而 AtomicInteger 類提供的 atomic 方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它資料型別和參考變數也可以進行相似操作,
15.為什么執行緒通信的方法 wait(), notify()和 notifyAll()被定義在 Object 類里?
Java 的每個物件中都有一個鎖(monitor,也可以成為監視器) 并且 wait(),notify()等方法用于等待物件的鎖或者通知其他執行緒物件的監視器可用,
在 Java 的執行緒中并沒有可供任何物件使用的鎖和同步器,這就是為什么這些方法是 Object 類的一部分,這樣 Java 的每一個類都有用于執行緒間通信的基本方法,
16.為什么 Thread 類的 sleep()和 yield ()方法是靜態的?
Thread 類的 sleep()和 yield()方法將在當前正在執行的執行緒上運行,所以在其他處于等待狀態的執行緒上呼叫這些方法是沒有意義的,這就是為什么這些方法是靜態的,它們可以在當前正在執行的執行緒中作業,并避免程式員錯誤的認為可以在其他非運行執行緒呼叫這些方法,
17.并發編程三要素?
- 原子性指的是一個或者多個操作,要么全部執行并且在執行的程序中不被其他操作打斷,要么就全部都不執行,
- 可見性指多個執行緒操作一個共享變數時,其中一個執行緒對變數進行修改后,其他執行緒可以立即看到修改的結果,
- 有序性,即程式的執行順序按照代碼的先后順序來執行,
18.Executors 類是什么?
Executors 為 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 Callable 類提供了一些工具方法,Executors 可以用于方便的創建執行緒池
第二關:小試牛刀
1.如何優雅的設定睡眠時間
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
2.如何停止一個執行緒
1.使用退出標志使執行緒正常退出
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
2.使用stop方法不過該方法已經被標記為過時的方法(因為會造成死鎖)
3.使用interrupt()方法中斷執行緒
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
if (Thread.interrupted()) {
System.out.println("執行緒被停止了,我要退出");
try {
throw new InterruptedException();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("執行緒已經被停止了");
}
}
}
}, "");
thread.start();
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
thread.interrupt();
}
}
3.yield()方法和join()的作用
yield()方法
放棄當前cpu資源,將它讓給其他的任務占用cpu執行時間,但放棄的時間不確定,有可能剛剛放棄,馬上又獲得cpu時間片,
join()方法
join是指把指定的執行緒加入到當前執行緒,比如join某個執行緒a, 會讓當前執行緒b進入等待,直到a的生命周期結束,此期間b執行緒是處于blocked狀態,
public class TestThread {
public static void main(String[] args) throws Exception{
Thread thread = new Thread(() -> {
try {
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
}finally {
System.out.println(Thread.currentThread().getName());
}
}, "a");
thread.start();
Thread.currentThread().join();
System.out.println("等待a執行緒執行完成才會執行");
}
}
4.執行緒的優先級
在作業系統中,執行緒可以劃分優先級,優先級較高的執行緒得到cpu資源比較多,也就是cpu有限執行優先級較高的執行緒物件中的任務,但是不能保證一定 優先級高,就先執行,
Java的優先級分為1~ 10個等級,數字越大優先級越高,默認優先級大小為5,超出范圍則拋出:
java.lang. IlegalArgumentException.
執行緒的優先級具有繼承性,比如a執行緒啟動b執行緒,b執行緒與a優先級是一樣的,
5.interrupted方法和isInterrupted方法的區別?
//該方法是判斷當前執行緒是否中斷(即執行該方法的執行緒)
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
//該方法是指this關鍵字所在類的物件是否中斷
public boolean isInterrupted() {
return isInterrupted(false);
}
舉一個例子
ThreadA threadA=new ThreadA();
threadA.interrupt();
System.out.println(threadA.interrupted());//false 判斷的是主執行緒main
System.out.println(threadA.isInterrupted());//true 判斷的是threadA執行緒
6.Java虛擬機退出時Daemon執行緒中的finally塊一定會執行嗎?
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
}finally {
System.out.println(Thread.currentThread().getName());
}
}, "aaaa");
thread.setDaemon(true);
thread.start();
}
}
控制臺沒有任何輸出說明finally中的陳述句沒有執行
7.設定執行緒背景關系類加載器
public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader()
8.什么是原子操作?
不可中斷的一個或一系列操作
8.并發和并行
-
并發:一個處理器同時處理多個任務,
-
并行:多個處理器或者是多核的處理器同時處理多個不同的任務.前者是邏輯上的同時發生(simultaneous),而后者是物理上的同時發生.
并發性(concurrency),又稱共行性,是指能處理多個同時性活動的能力,并發事件之間不一定要同一時刻發生,
并行(parallelism)是指同時發生的兩個并發事件,具有并發的含義,而并發則不一定并行,
來個比喻:并發和并行的區別就是一個人同時吃三個饅頭和三個人同時吃三個饅頭,
9.什么是多執行緒中的背景關系切換?
多執行緒會共同使用一組計算機上的 CPU,而執行緒數大于給程式分配的 CPU 數量時,為了讓各個執行緒都有執行的機會,就需要輪轉使用 CPU,不同的執行緒切換使用 CPU發生的切換資料等就是背景關系切換,
10.死鎖與活鎖的區別,死鎖與饑餓的區別?
死鎖: 是指兩個或兩個以上的行程(或執行緒)在執行程序中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去,
產生死鎖的必要條件:
1、互斥條件:所謂互斥就是行程在某一時間內獨占資源,
2、請求與保持條件:一個行程因請求資源而阻塞時,對已獲得的資源保持不放,
3、不剝奪條件:行程已獲得資源,在末使用完之前,不能強行剝奪, 4、回圈等待條件:若干行程之間形成一種頭尾相接的回圈等待資源關系,
活鎖: 任務或者執行者沒有被阻塞,由于某些條件沒有滿足,導致一直重復嘗試,失敗,嘗試,失敗,
活鎖和死鎖的區別在于,處于活鎖的物體是在不斷的改變狀態,所謂的“活”, 而處于死鎖的物體表現為等待;活鎖有可能自行解開,死鎖則不能,
饑餓: 一個或者多個執行緒因為種種原因無法獲得所需要的資源,導致一直無法執行的狀態,
Java 中導致饑餓的原因:
- 高優先級執行緒吞噬所有的低優先級執行緒的 CPU 時間,
- 執行緒被永久堵塞在一個等待進入同步塊的狀態,因為其他執行緒總是能在它之前持續地對該同步塊進行訪問,
- 執行緒在等待一個本身也處于永久等待完成的物件(比如呼叫這個物件的 wait 方法),因為其他執行緒總是被持續地獲得喚醒,
11.Java 中用到的執行緒調度演算法是什么?
采用時間片輪轉的方式,可以設定執行緒的優先級,會映射到下層的系統上面的優先級上,如非特別需要,盡量不要用,防止執行緒饑餓,
12.什么是執行緒組,為什么在 Java 中不推薦使用?
ThreadGroup 類,可以把執行緒歸屬到某一個執行緒組中,執行緒組中可以有執行緒物件,也可以有執行緒組,組中還可以有執行緒,這樣的組織結構有點類似于樹的形式,
為什么不推薦使用?因為使用有很多的安全隱患吧,沒有具體追究,如果需要使用,推薦使用執行緒池,
13.sleep 與 wait 的區別
-
對于 sleep()方法,我們首先要知道該方法是屬于 Thread 類中的,而 wait()方法,則是屬于Object 類中的,
-
sleep()方法導致了程式暫停執行指定的時間,讓出 cpu執行其他執行緒,但是他的監控狀態依然保持著,當指定的時間到了又會自動恢復運行狀態,
-
在呼叫 sleep()方法的程序中,執行緒不會釋放物件鎖,
-
而當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件呼叫 notify()方法后本執行緒才進入物件鎖定池準備獲取物件鎖進入運行狀態,
14.Java后臺執行緒
- 定義:守護執行緒–也稱“服務執行緒”,他是后臺執行緒,它有一個特性,即為用戶執行緒提供公共服務,在沒有用戶執行緒可服務時會自動離開,
- 優先級:守護執行緒的優先級比較低,用于為系統中的其它物件和執行緒提供服務,
- 設定:通過 setDaemon(true)來設定執行緒為“守護執行緒”;將一個用戶執行緒設定為守護執行緒的方式是在 執行緒物件創建 之前 用執行緒物件的 setDaemon 方法,
- 在 Daemon 執行緒中產生的新執行緒也是 Daemon 的,
- 執行緒則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個執行緒,這個執行緒的生命周期并不會和 Web 應用程式保持同步,也就是說,即使你停止了 Web 應用,這個執行緒依舊是活躍的,
- example: 垃圾回收執行緒就是一個經典的守護執行緒,當我們的程式中不再有任何運行的Thread,程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收執行緒是 JVM 上僅剩的執行緒時,垃圾回收執行緒會自動離開,它始終在低級別的狀態中運行,用于實時監控和管理系統中的可回收資源,
- 生命周期:守護行程(Daemon)是運行在后臺的一種特殊行程,它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件,也就是說守護執行緒不依賴于終端,但是依賴于系統,與系統“同生共死”,當 JVM 中所有的執行緒都是守護執行緒的時候,JVM 就可以退出了;如果還有一個或以上的非守護執行緒則 JVM 不會退出,
15.死鎖案例和死鎖分析

public class TestDeadLock {
public static void main(String[] args) {
MyResources resources = new MyResources();
new Thread(()->{
resources.printA();
},"A").start();
new Thread(()->{
resources.printB();
},"B").start();
}
}
class MyResources{
public String A="A";
public String B="B";
public void printA(){
synchronized (this.A){
System.out.println(Thread.currentThread().getName()+"\t 輸出AA");
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (this.B){
System.out.println(Thread.currentThread().getName()+"\t 輸出AA");
}
}
}
public void printB(){
synchronized (this.B){
System.out.println(Thread.currentThread().getName()+"\t 輸出BB");
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (this.A){
System.out.println(Thread.currentThread().getName()+"\t 輸出BB");
}
}
}
}
分析:jps -l+jstack
C:\Users>jps -l
12516
30168 chapter10.TestDeadLock
33128 org.jetbrains.jps.cmdline.Launcher
34860 jdk.jcmd/sun.tools.jps.Jps
C:\Users>jstack 30168
2021-09-12 09:46:14
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.10+8-LTS-162 mixed mode):
Found one Java-level deadlock:
=============================
"A":
waiting to lock monitor 0x000001b9aa5a2980 (object 0x0000000089f95e08, a java.lang.String),
which is held by "B"
"B":
waiting to lock monitor 0x000001b9ab178700 (object 0x0000000089f95dd8, a java.lang.String),
which is held by "A"
Java stack information for the threads listed above:
===================================================
"A":
at chapter10.MyResources.printA(TestDeadLock.java:27)
- waiting to lock <0x0000000089f95e08> (a java.lang.String)
- locked <0x0000000089f95dd8> (a java.lang.String)
at chapter10.TestDeadLock.lambda$main$0(TestDeadLock.java:9)
at chapter10.TestDeadLock$$Lambda$14/0x0000000100066840.run(Unknown Source)
at java.lang.Thread.run(java.base@11.0.10/Thread.java:834)
"B":
at chapter10.MyResources.printB(TestDeadLock.java:36)
- waiting to lock <0x0000000089f95dd8> (a java.lang.String)
- locked <0x0000000089f95e08> (a java.lang.String)
at chapter10.TestDeadLock.lambda$main$1(TestDeadLock.java:13)
at chapter10.TestDeadLock$$Lambda$15/0x0000000100066c40.run(Unknown Source)
at java.lang.Thread.run(java.base@11.0.10/Thread.java:834)
Found 1 deadlock.
16.ThreadLocal 作用(執行緒本地存盤)
ThreadLocal,很多地方叫做執行緒本地變數,也有些地方叫做執行緒本地存盤,ThreadLocal 的作用是提供執行緒內的區域變數,這種變數在執行緒的生命周期內起作用,減少同一個執行緒內多個函式或者組件之間一些公共變數的傳遞的復雜度,
使用場景
最常見的 ThreadLocal 使用場景為 用來解決 資料庫連接、Session 管理等,
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
更多內容可以看:ThreaLocal詳細解讀
17.Java 中用到的執行緒調度
搶占式調度:搶占式調度指的是每條執行緒執行的時間、執行緒的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條執行緒都分同樣的執行時間片,也可能是某些執行緒執行的時間片較長,甚至某些執行緒得不到執行的時間片,在這種機制下,一個執行緒的堵塞不會導致整個行程堵塞,
協同式調度:協同式調度指某一執行緒執行完后主動通知系統切換到另一執行緒上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑,執行緒的執行時間由執行緒本身控制,執行緒切換可以預知,不存在多執行緒同步問題,但它有一個致命弱點:如果一個執行緒撰寫有問題,運行到一半就一直堵塞,那么可能導致整個系統崩潰,
JVM 的執行緒調度實作(搶占式調度)
java 使用的執行緒調使用搶占式調度,Java 中執行緒會按優先級分配 CPU 時間片運行,且優先級越高越優先執行,但優先級高并不代表能獨自占用執行時間片,可能是優先級高得到越多的執行時間片,反之,優先級低的分到的執行時間少但不會分配不到執行時間,
執行緒讓出 cpu 的情況:
- 當前運行執行緒主動放棄 CPU,JVM 暫時放棄 CPU 操作(基于時間片輪轉調度的 JVM 作業系統不會讓執行緒永久放棄 CPU,或者說放棄本次時間片的執行權),例如呼叫 yield()方法,
- 當前運行執行緒因為某些原因進入阻塞狀態,例如阻塞在 I/O 上,
- 當前運行執行緒結束,即運行完 run()方法里面的任務,
18.一個執行緒運行時發生例外會怎樣?
如果例外沒有被捕獲該執行緒將會停止執行,Thread.UncaughtExceptionHandler是用于處理未捕獲例外造成執行緒突然中斷情況的一個內嵌介面,當一個未捕獲例外將造成執行緒中斷的時候 JVM 會使用 Thread.getUncaughtExceptionHandler()來查詢執行緒的 UncaughtExceptionHandler 并將執行緒和例外作為引數傳遞給handler 的 uncaughtException()方法進行處理,
19.同步方法和同步塊,哪個是更好的選擇?
同步塊是更好的選擇,因為它不會鎖住整個物件(當然你也可以讓它鎖住整個物件),同步方法會鎖住整個物件,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行并需要等待獲得這個物件上的鎖,
同步塊更要符合開放呼叫的原則,只在需要鎖住的代碼塊鎖住相應的物件,這樣從側面來說也可以避免死鎖,
20.多執行緒的價值?
1、發揮多核 CPU 的優勢
多執行緒,可以真正發揮出多核 CPU 的優勢來,達到充分利用 CPU 的目的,采用多執行緒的方式去同時完成幾件事情而不互相干擾,
2、防止阻塞
從程式運行效率的角度來看,單核 CPU 不但不會發揮出多執行緒的優勢,反而會因為在單核 CPU 上運行多執行緒導致執行緒背景關系的切換,而降低程式整體的效率,但是單核 CPU 我們還是要應用多執行緒,就是為了防止阻塞,試想,如果單核 CPU 使用單執行緒,那么只要這個執行緒阻塞了,比方說遠程讀取某個資料吧,對端遲遲未回傳又沒有設定超時時間,那么你的整個程式在資料回傳回來之前就停止運行了,多執行緒可以防止這個問題,多條執行緒同時運行,哪怕一條執行緒的代碼執行讀取資料阻塞,也不會影響其它任務的執行,
3、便于建模
這是另外一個沒有這么明顯的優點了,假設有一個大的任務 A,單執行緒編程,那么就要考慮很多,建立整個程式模型比較麻煩,但是如果把這個大的任務 A 分解成幾個小任務,任務 B、任務 C、任務 D,分別建立程式模型,并通過多執行緒分別運行這幾個任務,那就簡單很多了,
21. 創建執行緒的三種方式的對比?
1、采用實作 Runnable、Callable 介面的方式創建多執行緒,
優勢是:執行緒類只是實作了 Runnable 介面或 Callable 介面,還可以繼承其他類,在這種方式下,多個執行緒可以共享同一個 target 物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將 CPU、代碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想,
劣勢是:編程稍微復雜,如果要訪問當前執行緒,則必須使用 Thread.currentThread()方法,
2、使用繼承 Thread 類的方式創建多執行緒
優勢是:撰寫簡單,如果需要訪問當前執行緒,則無需使用 Thread.currentThread()方法,直接使用 this 即可獲得當前執行緒,
劣勢是:執行緒類已經繼承了 Thread 類,所以不能再繼承其他父類,
3、Runnable 和 Callable 的區別
-
Callable 規定(重寫)的方法是 call(),Runnable 規定(重寫)的方法是 run(),
-
Callable 的任務執行后可回傳值,而 Runnable 的任務是不能回傳值的,
-
Call 方法可以拋出例外,run 方法不可以,
-
運行 Callable 任務可以拿到一個 Future 物件,表示異步計算的結果,它提供了檢查計算是否完成的方法,以等待計算的完成,并檢索計算的結果,通過 Future物件可以了解任務執行情況,可取消任務的執行,還可獲取執行結果,
22. Java 執行緒數過多會造成什么后果?
1、執行緒的生命周期開銷非常高
2、消耗過多的 CPU 資源
如果可運行的執行緒數量多于可用處理器的數量,那么有執行緒將會被閑置,大量空閑的執行緒會占用許多記憶體,給垃圾回收器帶來壓力,而且大量的執行緒在競爭 CPU資源時還將產生其他性能的開銷,
3、降低穩定性
JVM 在可創建執行緒的數量上存在一個限制,這個限制值將隨著平臺的不同而不同,并且承受著多個因素制約,包括 JVM 的啟動引數、Thread 建構式中請求堆疊的大小,以及底層作業系統對執行緒的限制等,如果破壞了這些限制,那么可能拋出OutOfMemoryError 例外,
23.執行緒的調度策略
執行緒調度器選擇優先級最高的執行緒運行,但是,如果發生以下情況,就會終止執行緒的運行:
- 執行緒體中呼叫了 yield 方法讓出了對 cpu 的占用權利
- 執行緒體中呼叫了 sleep 方法使執行緒進入睡眠狀態
- 執行緒由于 IO 操作受到阻塞
- 另外一個更高優先級執行緒出現
- 在支持時間片的系統中,該執行緒的時間片用完
24.什么是 Future?
在并發編程中,我們經常用到非阻塞的模型,在之前的多執行緒的三種實作中,不管是繼承 thread 類還是實作 runnable 介面,都無法保證獲取到之前的執行結果,
通過實作 Callback 介面,并用 Future 可以來接收多執行緒的執行結果,Future 表示一個可能還沒有完成的異步任務的結果,針對這個結果可以添加Callback 以便在任務執行成功或失敗后作出相應的操作,
25.寫一個Callable案例
public class TestFuture {
public static void main(String[] args) throws Exception{
FutureTask<String> task=new FutureTask<>(new CallableThread());
task.run();
System.out.println(task.get());
}
}
class CallableThread implements Callable<String> {
@Override
public String call() throws InterruptedException {
System.out.println("hello world");
return "hello world";
}
}
第三關:過關斬將
1.synchronized底層的兩個jvm指令
monitor enter 和 monitor exit
2.java物件頭
synchronized用的鎖是存在Java物件頭里的,物件如果是陣列型別,虛擬機用3個字寬(Word)存盤物件頭,如果物件是非陣列型別,用2字寬存盤物件頭,
| 長度 | 內容 | 說明 |
|---|---|---|
| 32/64bit | Mark Word | 存盤物件的hashCode或鎖資訊等 |
| 32/64bit | Class Metadata Address | 存盤到物件型別資料的指標 |
| 32/64bit | Array length | 陣列的長度(如果當前物件是陣列) |

3.鎖的升降級規則
Java SE1.6為了提高鎖的性能,引入了“偏向鎖”和輕量級鎖”,
Java SE 1.6中鎖有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,
鎖只能升級不能降級,
4.偏向鎖
Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一線 程多次獲得,偏向鎖的目的是在某個執行緒獲得鎖之后,消除這個執行緒鎖重入(CAS)的開銷,看起 來讓這個執行緒得到了偏護,引入偏向鎖是為了在無多執行緒競爭的情況下盡量減少不必要的輕量級 鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令(由于一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所 以偏向鎖的撤銷操作的性能損耗必須小于節省下來的 CAS 原子指令的性能消耗),上面說過,輕 量級鎖是為了在執行緒交替執行同步塊時提高性能,而偏向鎖則是在只有一個執行緒執行同步塊時進 一步提高性能,
//設定jvm引數用來關閉偏向鎖,鎖會變為輕量級鎖
-XX:-UseBiasedLocking=false
//關閉偏向鎖延遲
-XX:BiasedLockingStartupDelay=0
5.輕量級鎖
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖, 鎖升級 隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的, 也就是說只能從低到高升級,不會出現鎖的降級), “輕量級”是相對于使用作業系統互斥量來實作的傳統鎖而言的,但是,首先需要強調一點的是, 輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量 級鎖使用產生的性能消耗,在解釋輕量級鎖的執行程序之前,先明白一點,輕量級鎖所適應的場 景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹 為重量級鎖,
6.重量級鎖
Synchronized 是通過物件內部的一個叫做監視器鎖(monitor)來實作的,但是監視器鎖本質又 是依賴于底層的作業系統的 Mutex Lock 來實作的,而作業系統實作執行緒之間的切換這就需要從用 戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什么 Synchronized 效率低的原因,
因此,這種依賴于作業系統 Mutex Lock 所實作的鎖我們稱之為 “重量級鎖”,JDK 中對 Synchronized 做的種種優化,其核心都是為了減少這種重量級鎖的使用, JDK1.6 以后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了“輕量級鎖”和 “偏向鎖”,
7.鎖優化
減少鎖持有時間
只用在有執行緒安全要求的程式上加鎖
減小鎖粒度
將大物件(這個物件可能會被很多執行緒訪問),拆成小物件,大大增加并行度,降低鎖競爭,降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高,最最典型的減小鎖粒度的案例就是ConcurrentHashMap,
鎖分離
最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了執行緒安全,又提高了性能,讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離,比如LinkedBlockingQueue 從頭部取出,從尾部放資料,
鎖粗化
通常情況下,為了保證多執行緒間的有效并發,會要求每個執行緒持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖,但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利于性能的優化 ,
鎖消除
鎖消除是在編譯器級別的事情,在即時編譯器時,如果發現不可能被共享的物件,則可以消除這些物件的鎖操作,多數是因為程式員編碼不規范引起,
8.鎖對比
| 鎖 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 | 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一 個執行緒訪問同步塊場景 |
| 輕量級鎖 | 競爭的執行緒不會阻塞,提高了程式的回應速度 | 如果始終得不到鎖競爭的執行緒,使用自旋會消耗CPU | 追求回應時間同步塊執行速度非常快 |
| 重量級鎖 | 執行緒競爭不使用自旋,不會消耗CPU | 執行緒阻塞,回應時間緩慢 | 追求吞吐量同步塊執行速度較長 |
9.生產者消費者模型
實作如下操作執行緒一加一減
product1 正在操作Number=1
consumer2 正在操作 Number=0
product2 正在操作Number=1
consumer4 正在操作 Number=0
product4 正在操作Number=1
consumer5 正在操作 Number=0
product5 正在操作Number=1
consumer1 正在操作 Number=0
product3 正在操作Number=1
consumer3 正在操作 Number=0
1.0版本synchronized+wait+notifyAll
public class ProductConsumerLock {
public static void main(String[] args) {
Sources source = new Sources();
for (int i = 1; i <= 5; i++) {
new Thread(()->{
try {
source.addNumber();
} catch (Exception e) {
e.printStackTrace();
}
},"product"+i).start();
}
for (int i = 1; i <= 5; i++) {
new Thread(()->{
try {
source.subNumber();
} catch (Exception e) {
e.printStackTrace();
}
},"consumer"+i).start();
}
}
}
class Source{
public volatile int number=0;
public synchronized void addNumber() throws Exception{
while (number==1){
this.wait();
}
number+=1;
System.out.println(Thread.currentThread().getName()+"\t正在操作Number="+number);
this.notifyAll();
}
public synchronized void subNumber() throws Exception{
while (number==0){
this.wait();
}
number-=1;
System.out.println(Thread.currentThread().getName()+"\t正在操作\tNumber="+number);
this.notifyAll();
}
}
2.0版本lock+await+sginalall
class Sources{
public volatile int number=0;
public Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
public void addNumber() throws Exception{
try {
lock.lock();
while (number==1){
condition.await();
}
number+=1;
System.out.println(Thread.currentThread().getName()+"\t正在操作Number="+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
public synchronized void subNumber() throws Exception{
try {
lock.lock();
while (number==0){
condition.await();
}
number-=1;
System.out.println(Thread.currentThread().getName()+"\t正在操作\tNumber="+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
10.synchronized和lock的區別lock有什么好處?
1.原始構成:
synchronized是關鍵字屬于JVM層面,
monitorenter(底層是通過monitor物件來完成,其義wait/notify等方法也依賴于monitor物件只有在同步塊或方法中才能調wait/notify
monitorexit
Lock是具體類( java. util. concurrent . locks . Lock)是api層面的鎖
2.使用方法:
synchronized不需要用戶去手動釋放鎖,當synchronized代碼執行完后系統會自動讓執行緒釋放對鎖的占用,
Reentrantlock則需要用戶去手動釋放鎖若沒有主動釋放鎖,就有可能導致出現死鎖現象,
需要Llock()和unlock()方法配合try/finally陳述句塊來完成,
3.等待是否可中斷
synchronized不可中斷,除非拋出例外或者正常運行完成
ReentrantLock可中斷
- 設定超時方法tryLock(long timeout, TimeUnit unit)
- lockInterruptibly()放代碼塊中,呼叫interrupt() 方法可中斷
4.加鎖是否公平
synchronized非公平鎖
Reentrantlock兩者都可以,默認非公平鎖,構造方法可以傳入boolean值,true為公 平鎖,false 為非公平鎖
5.鎖系結多個條件condition
synchronized沒有
Reentrantlock用來實作分組喚醒需要喚醒的執行緒們,可以精確喚醒,而不是像synchronized要么隨機喚醒一個執行緒要么喚醒全部執行緒,
lock的優勢
整體上來說 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件佇列的(newCondition 方法)鎖操作,另外 Lock 的實作類基本都支持非公平鎖(默認)和公平鎖,synchronized 只支持非公平鎖,當然,在大部分情況下,非公平鎖是高效的選擇,
11.樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作,
java 中的樂觀鎖基本都是通過 CAS 操作實作的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗,
12.悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到并發寫的可能性高,每次去拿資料的時候都認為別人會修改,所以每次在讀寫資料的時候都會上鎖,這樣別人想讀寫這個資料就會 block 直到拿到鎖,
java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如 RetreenLock,
13.自旋鎖
自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那么那些等待競爭鎖的執行緒就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖后即可立即獲取鎖,這樣就避免用戶執行緒和內核的切換的消耗,
執行緒自旋是需要消耗 cpu 的,說白了就是讓 cpu在做無用功,如果一直獲取不到鎖,那執行緒也不能一直占用 cpu 自旋做無用功,所以需要設定一個自旋等待的最大時間,
如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態,
自旋鎖的優缺點
自旋鎖盡可能的減少執行緒的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于執行緒阻塞掛起再喚醒的操作的消耗,這些操作會導致執行緒發生兩次背景關系切換!
但是如果鎖的競爭激烈,或者持有鎖的執行緒需要長時間占用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用 cpu 做無用功,占著 XX 不 XX,同時有大量執行緒在競爭一個鎖,會導致獲取鎖的時間很長,執行緒自旋的消耗大于執行緒阻塞掛起操作的消耗,其它需要 cpu的執行緒又不能獲取到 cpu,造成 cpu 的浪費,所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)
自旋鎖的目的是為了占著 CPU 的資源不釋放,等到獲取到鎖立即進行處理,但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的執行緒處于自旋狀態占用 CPU 資源,進而會影響整體系統的性能,因此自旋的周期選的額外重要!
JVM 對于自旋周期的選擇,jdk1.5 這個限度是一定的寫死的,在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認為一個執行緒背景關系切換的時間是最佳的一個時間,同時 JVM 還針對當前 CPU 的負荷情況做了較多的優化,如果平均負載小于 CPUs 則一直自旋,如果有超過(CPUs/2)個執行緒正在自旋,則后來執行緒直接阻塞,如果正在自旋的執行緒發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞,如果 CPU 處于節電模式則停止自旋,自旋時間的最壞情況是 CPU的存盤延遲(CPU A 存盤了一個資料,到 CPU B 得知這個資料直接的時間差),自旋時會適當放棄執行緒優先級之間的差異,
自旋鎖的開啟
JDK1.6 中-XX:+UseSpinning 開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7 后,去掉此引數,由 jvm 控制;
14.可重入鎖(遞回鎖)
最大作用就是避免死鎖
指的是同一執行緒外層函式獲得鎖之后,內層遞回函式仍然能獲取該鎖的代碼,在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖也即是說,執行緒可以進入任何一個它已經擁有的鎖所同步著的代碼塊,
在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖,
public class Phone {
public static synchronized void sendSMS(){
System.out.println(Thread.currentThread().getId()+"sendSMS() invoked");
sendEmail();
}
public static synchronized void sendEmail(){
System.out.println(Thread.currentThread().getId()+"sendEmail() invoked");
}
public static void main(String[] args) {
new Thread(()->{
sendSMS();
}).start();
new Thread(()->{
sendSMS();
}).start();
}
}
14sendSMS() invoked
14sendEmail() invoked
15sendSMS() invoked
15sendEmail() invoked
ReentrantLock 版本
public void set(){
Lock lock=new ReentrantLock();
lock.lock();
System.out.println(Thread.currentThread().getId()+"set() invoked");
get();
lock.unlock();
}
public void get(){
Lock lock=new ReentrantLock();
lock.lock();
System.out.println(Thread.currentThread().getId()+"get() invoked");
lock.unlock();
}
注意:lock()和unlock()要配對 可以使用多個
//正常
public void get(){
Lock lock=new ReentrantLock();
lock.lock();
lock.lock();
System.out.println(Thread.currentThread().getId()+"get() invoked");
lock.unlock();
lock.unlock();
}
//不正常 后面的持有鎖的方法代碼不會執行因為為釋放鎖
public void get(){
Lock lock=new ReentrantLock();
lock.lock();
lock.lock();
System.out.println(Thread.currentThread().getId()+"get() invoked");
lock.unlock();
}
15.公平鎖與非公平鎖
公平鎖(Fair)
加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
- 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個佇列
- Java 中的 synchronized 是非公平鎖,ReentrantLock 默認的 lock()方法采用的是非公平鎖,
16.ReadWriteLock 讀寫鎖
為了提高性能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程式的執行效率,讀寫鎖分為讀鎖和寫
鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可,
讀鎖:如果你的代碼只讀資料,可以很多人同時讀,但不能同時寫,那就上讀鎖
寫鎖:如果你的代碼修改資料,只能有一個人在寫,且不能同時讀取,那就上寫鎖,總之,讀的時候上
讀鎖,寫的時候上寫鎖!
public class ReadWriteLockDemo {
/*
* 讀讀共享
* 讀寫互斥
* 寫寫互斥
* */
public static void main(String[] args) {
MyCache myCache = new MyCache();
//寫執行緒
for (int i = 0; i < 5; i++) {
final String temp=i+"";
new Thread(()->{
myCache.put(temp,temp);
},String.valueOf(i)).start();
}
//讀執行緒
for (int i = 0; i < 5; i++) {
final String temp=i+"";
new Thread(()->{
myCache.get(temp);
},String.valueOf(i)).start();
}
}
}
class MyCache{
private volatile Map<String,Object> map=new HashMap<>();
//讀寫鎖
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//添加資料
public void put(String key,Object value){
Lock lock = readWriteLock.writeLock();
lock.lock();
System.out.println("執行緒"+Thread.currentThread().getName()+"\t正在寫入");
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
map.put(key,value);
System.out.println("執行緒"+Thread.currentThread().getName()+"\t寫入完成");
lock.unlock();
}
//查詢資料
public Object get(String key){
Lock lock = readWriteLock.readLock();
lock.lock();
System.out.println("執行緒"+Thread.currentThread().getName()+"\t正在讀取");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
Object result = map.get(key);
System.out.println("執行緒"+Thread.currentThread().getName()+"\t讀取完成");
lock.unlock();
return result;
}
}
17.共享鎖和獨占鎖
獨占鎖
獨占鎖模式下,每次只能有一個執行緒能持有鎖,ReentrantLock 就是以獨占方式實作的互斥鎖,
獨占鎖是一種悲觀保守的加鎖策略,它避免了讀/讀沖突,如果某個只讀執行緒獲取鎖,則其他讀執行緒都只能等待,這種情況下就限制了不必要的并發性,因為讀操作并不會影響資料的一致性,
共享鎖
共享鎖則允許多個執行緒同時獲取鎖,并發訪問 共享資源,如:ReadWriteLock,共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的執行緒同時訪問共享資源,
- AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 佇列中等待執行緒的鎖獲取模式,
- java 的并發包中提供了 ReadWriteLock,讀-寫鎖,它允許一個資源可以被多個讀操作訪問,或者被一個 寫操作訪問,但兩者不能同時進行,
18.分段鎖
分段鎖也并非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐,
ConcurrentHashMap,它內部細分了若干個小的 HashMap,稱之為段(Segment),默認情況下一個 ConcurrentHashMap 被進一步細分為 16 個段,既就是鎖的并發度,
如果需要在 ConcurrentHashMap 中添加一個新的表項,并不是將整個 HashMap 加鎖,而是首先根據 hashcode 得到該表項應該存放在哪個段中,然后對該段加鎖,并完成 put 操作,在多執行緒環境中,如果多個執行緒同時進行 put操作,只要被加入的表項不存放在同一個段中,則執行緒間可以做到真正的并行,
ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成
ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成,Segment 是一種可重入鎖 ReentrantLock,在ConcurrentHashMap 里扮演鎖的角色,HashEntry 則用于存盤鍵值對資料,一個 ConcurrentHashMap 里包含一個 Segment 陣列,Segment 的結構和 HashMap類似,是一種陣列和鏈表結構, 一個 Segment 里包含一個 HashEntry 陣列,每個 HashEntry 是
一個鏈表結構的元素, 每個 Segment 守護一個 HashEntry 陣列里的元素,當對 HashEntry 陣列的資料進行修改時,必須首先獲得它對應的 Segment 鎖,

19.tryLock 和 lock 和 lockInterruptibly 的區別
- tryLock 能獲得鎖就回傳 true,不能就立即回傳 false,tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,回傳 false
- lock 能獲得鎖就回傳 true,不能的話一直等待獲得鎖
- lock 和 lockInterruptibly,如果兩個執行緒分別執行這兩個方法,但此時中斷這兩個執行緒, lock 不會拋出例外,而 lockInterruptibly 會拋出例外,
20.什么是作業竊取演算法?
是指某個執行緒從其他佇列里竊取任務來執行,當大任務被分割成小任務時,有的執行緒可能提前完成任務,此時閑著不如去幫其他沒完成作業執行緒,此時可以去其他佇列竊取任務,為了減少競爭,通常使用雙端佇列,被竊取的執行緒從頭部拿,竊取的執行緒從尾部拿任務執行,
作業竊取演算法的有缺點
優點:充分利用執行緒進行并行計算,減少了執行緒間的競爭,
缺點:有些情況下還是存在競爭,比如雙端佇列中只有一個任務,這樣就消耗了更多資源,
21.Java中的原子更新類
原子操作更新基本型別
AtomicBoolean:原子更新布爾型別
AtonicInteger:原子更新整型
AtomicLong:原子更新長整型
原子操作更新陣列
AtomicIntegerArray:原子更新整型資料里的元素
AtomicLongArray:原子更新長整型陣列里的元素
AtomicReferenceArray:原子更新參考型別陣列里的元素
AtomicIntegerArray:主要提供原子方式更新陣列里的整型
原子操作更新參考型別
如果原子需要更新多個變數,就需要用參考型別了,
AtomicReference:原子更新參考型別
AtomicReferenceFieldUpdater:原子更新參考型別里的欄位,
AtomicMarkableReference:
原子更新帶有標記位的參考型別,標記位用boolean型別表示,構造方法時
AtomicMarkableReference(V initialRef,boolean initialMark)
原子操作更新欄位類
AtomiceIntegerFieldUpdater:原子更新整型欄位的更新器
AtomiceLongFieldUpdater:原子更新長整型欄位的更新器
AtomiceStampedFieldUpdater:原子更新帶有版本號的參考型別,將整數值
22.SynchronizedMap 和 ConcurrentHashMap 有什么區別?
SynchronizedMap 一次鎖住整張表來保證執行緒安全,所以每次只能有一個執行緒來訪為 map,
ConcurrentHashMap 使用分段鎖來保證在多執行緒下的性能,ConcurrentHashMap 中則是一次鎖住一個桶,ConcurrentHashMap 默認將hash 表分為 16 個桶,諸如 get,put,remove 等常用操作只鎖當前需要用到的桶,這樣,原來只能一個執行緒進入,現在卻能同時有 16 個寫執行緒執行,并發性能的提升是顯而易見的,另外 ConcurrentHashMap 使用了一種不同的迭代方式,在這種迭代方式中,當iterator 被創建后集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時 new 新的資料從而不影響原有的資料 ,iterator 完成后再將頭指標替換為新的資料 ,這樣 iterator執行緒可以使用原來老的資料,而寫執行緒也可以并發的完成改變,
23.多執行緒與單例模式
多執行緒與單例模式
24.什么是 Java Timer 類?如何創建一個有特定時間間隔的任務?
全面了解Java Timer定時器類
第四關:登峰造極
1.什么是CAS
CAS的全稱為Compare-And-Swap(比較并交換),它是一條CPU并發原語,
它的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個程序是原子的,
CAS并發原語體現在JAVA語言中就是sun.misc.Unsafe類中的各個方法,呼叫UnSafe類中的CAS方法,JVM會幫我們實作出CAS匯編指令,
這是一種完全依賴于硬體的功能,通過它實作了原子操作,再次強調,由于CAS是一種系統原語,原語屬于作業系統用語范疇,是由若干條指令組成的,用于完成某個功能的一個程序,并且原語的執行必須是連續的,在執行程序中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的資料不一致問題,
2.UnSafe類(jdk.internal.misc.UnSafe)
是CAS的核心類,由于Java 方法無法直接訪問底層系統,需要通過本地(native) 方法來訪問,Unsafe相當于一個后門,基于該類可以直接操作特定記憶體的資料,Unsafe 類存在于sun.misc包中,其內部方法操作可以像C的指標一樣直接操作記憶體,因為Java中CAS操作的執行依賴于Unsafe類的方法,
注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接呼叫作業系統底層資源執行相應任務,
3.CAS原理分析
用AtomicInteger類中的方法來說明CAS的底層原理和思想
//原子上的加一操作
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
底層呼叫jdk.internal.misc.UnSafe類中的方法
//o-->AtomicInteger物件
//offset-->地址偏移量
//delta-->要加的數字
//v-->通過AtomicInteger物件和偏移地址得到的AtomicInteger在主記憶體具體數值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//通過AtomicInteger物件和偏移地址得到的AtomicInteger在主記憶體具體數值
v = getIntVolatile(o, offset);
//用該物件當前的值與v比較:
//如果相同,更新v + delta并且回傳true,
//如果不同,繼續取值然后再比較,直到更新完成,
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
舉例:假設執行緒A和執行緒B兩個執行緒同時執行getAndAddInt操作(分別跑在不同CPU上) :
- AtomicInteger里面的value原始值為3,即主記憶體中AtomicInteger的value為3, 根據JMM模型,執行緒A和執行緒B各自持有-份值為3的value的副本分別到各自的作業記憶體,
- 執行緒A通過getIntVolatile(var1, var2)拿到value值3, 這時執行緒A被掛起,
- 執行緒B也通過getlIntVolatile(var1, var2)方法獲取到value值3, 此時剛好執行緒B沒有被掛起并執行compareAndSwapInt方法比較記憶體值也為3,成功修改記憶體值為4,執行緒B打完收工,一切OK,
- 這時執行緒A恢復,執行compareAndSwapInt方法比較, 發現自己手里的值數字3和主記憶體的值數字4不一致, 說明該值已經被其它執行緒搶先一步修改過了,那A執行緒本次修改失敗,只能重新讀取重新來一遍了,
- 執行緒A重新獲取value值, 因為變數value被volatile修飾, 所以其它執行緒對它的修改,執行緒A總是能夠看到,執行緒A繼續執
行compareAndSwaplnt進行比較替換,直到成功,
4.CAS出現的問題分析與解決
1) ABA問題
CAS演算法實作一個重要前提需要取出記憶體中某時刻的資料并在當下時刻比較并替換,那么在這個時間差類會導致資料的變化,
比如說一個執行緒one從記憶體位置V中取出A,這時候另一個執行緒two也從記憶體中取出A,并且執行緒two進行了一些操作將值變成了B,然后執行緒two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然后執行緒one操作成功,
盡管執行緒one的CAS操作成功,但是不代表這個程序就是沒有問題的,
解決方案:添加版本號,每次更新的時候追加版本號,A-B-A- -> 1A-2B-3A,
從jdk1.5開始,Atomic包提供了一個類AtomicStampedReference來解決ABA的問題,
public class ABA {
static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
//初始化時間戳為1
static AtomicStampedReference<Integer> asr=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("引出ABA問題");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
System.out.println("解決ABA問題");
new Thread(()->{
int stamp= asr.getStamp();
//睡一秒保證t4能夠獲得和t3一樣的初始時間戳
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
asr.compareAndSet(100,101,1,asr.getStamp()+1);
asr.compareAndSet(101,100, asr.getStamp(), asr.getStamp()+1);
},"t3").start();
new Thread(()->{
int stamp= asr.getStamp();
//睡3秒保證t3的改變能夠被t4察覺
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = asr.compareAndSet(100, 230, stamp, asr.getStamp() + 1);
System.out.println("修改結果:"+result);
System.out.println("現在的值:"+asr.getReference());
System.out.println("現在的版本號:"+asr.getStamp());
},"t4").start();
}
}
引出ABA問題
解決ABA問題
true 2019
修改結果:false
現在的值:100
現在的版本號:3
2)回圈時間長開銷大(do while 回圈一直自旋直到修改成功為止,會給cpu帶來大開銷)
如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升,
原因一、它可以延遲流水線執行指令(de-pipeline),使cpu不會消耗過多的執行資源,延遲的時間取決于具體實作的版本,有些處理器延遲時間是0,
原因二、它可以避免在退出回圈的時候因記憶體順序沖突而引起的cpu流水線被清空,從而提高cpu執行效率,
3)只能保證一個變數的原子操作
一、對多個共享變數操作時,可以用鎖,
二、可以把多個共享變數合并成一一個共享變數來操作,比如,x=1,k=a, 合并xk=1a,然后用cas操作xk,
java 1.5開始;jdk提供了AtomicReference類來保證參考物件之間的原子性,就可以把多個變數放在一個物件來進行cas操作,
5.JMM模型(java記憶體模型規范不是真實存在)
必須滿足 原子性 可見性 有序性
由于JVM運行程式的物體是執行緒,而每個執行緒創建時JVM都會為其創建一個作業記憶體(有些地方稱為堆疊空間),作業記憶體是每個執行緒的私有資料區域,而Java記憶體模型中規定所有變數都存盤在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在作業記憶體中進行,首先要將變數從主記憶體拷貝的自己的作業記憶體空間,然后對變數進行操作,操作完成后再將變數寫回主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的作業記憶體中存盤著主記憶體中的變數副本拷貝,因此不同的執行緒間無法訪問對方的作業記憶體,執行緒間的通信(傳值)必須通過主記憶體來完成,其簡要訪問程序如下圖:


Java 的 內 存 模 型 定 義 了 8 種 內 存 間 操 作 :
lock 和 unlock
- 把 一 個 變 量 標 識 為 一 條 線 程 獨 占 的 狀 態 ,
- 把 一 個 處 于 鎖 定 狀 態 的 變 量 釋 放 出 來 ,釋 放 之 后 的 變 量 才 能 被 其 他 線 程 鎖定 ,
read 和 write
- 把 一 個 變 量 值 從 主 內 存 傳 輸 到 線 程 的 工 作 內 存 , 以 便 load,
- 把 store 操 作 從 工 作 內 存 得 到 的 變 量 的 值 , 放 入 主 內 存 的 變 量 中 ,
load 和 store
- 把 read 操 作 從 主 內 存 得 到 的 變 量 值 放 入 工 作 內 存 的 變 量 副 本 中 ,
- 把 工 作 內 存 的 變 量 值 傳 送 到 主 內 存 , 以 便 write,
use 和 assgin
- 把 工 作 內 存 變 量 值 傳 遞 給 執 行 引 擎 ,
- 將 執 行 引 擎 值 傳 遞 給 工 作 內 存 變 量 值 ,
6.如何實作一個自旋鎖
是指嘗試獲取鎖的執行緒不會立即阻塞,而是采用回圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒背景關系切換的消耗,缺點是回圈會消耗CPU
public class SpinLockDemo {
//原子執行緒參考
AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void myLock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"come in");
//開始自旋
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnLock(){
Thread thread=Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName()+"invoke myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.myUnLock();
},"AA").start();
//保證A先執行
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
spinLockDemo.myLock();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.myUnLock();
},"BB").start();
}
}
AAcome in
BBcome in
AAinvoke myUnlock()
BBinvoke myUnlock()
7.volatile的作用
JVM提供的輕量級鎖機制有以下三個特點
1)保證可見性
//驗證可見性
public class TestVolatile {
public volatile int number=0;
public void add(){
number=60;
}
public static void main(String[] args) {
TestVolatile testVolatile = new TestVolatile();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"start update");
try {
Thread.sleep(3000);
testVolatile.add();
System.out.println(Thread.currentThread().getName()+"over update");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
while (testVolatile.number==0){
}
System.out.println("等待輸出");
}
}
沒有volatile修飾變數的輸出
AAAstart update
AAAover update
有volatile修飾變數的輸出—》驗證了volatile能保證可見性
AAAstart update
等待輸出
AAAover update
2)不保證原子性
public static void main(String[] args) {
TestVolatile testVolatile = new TestVolatile();
for (int i = 0; i < 20; i++) {
//10個執行緒加number1000次
new Thread(()->{
for (int j = 0; j < 1000; j++) {
testVolatile.number++;
}
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("最終結果:"+testVolatile.number);
}
最終結果:19200
解決辦法:使用原子類AtomicInteger
3)禁止指令重排
計算機在執行程式時,為了提高性能,編譯器和處理器的常常會對指令做重排,一般分以下3種

單執行緒環境里面確保程式最終執行結果和代碼順序執行的結果一致,
處理器在進行重排序時必須要考慮指令之間的資料依賴性
//陳述句3依賴陳述句1
int X=11; //陳述句1
int Y= 12; //陳述句2
X=X+5;//陳述句3
Y=X*X;//陳述句4
指令可以重排為1234 2134 1324
但是要考慮資料之間的依賴性不能為4123
多執行緒環境中執行緒交替執行,由于編譯器優化重排的存在,兩個執行緒中使用的變數能否保證–致性是無法確定的,結果無法預測
8.執行緒池相關問題
執行緒池內容較多,可以參考我的另一篇文章:
看了就能學會的執行緒池技術
9.Synchronized 同步鎖分析
synchronized 它可以把任意一個非 NULL 的物件當作鎖,他屬于獨占式的悲觀鎖,同時屬于可重入鎖,
Synchronized 作用范圍
- 作用于方法時,鎖住的是物件的實體(this);
- 當作用于靜態方法時,鎖住的是Class實體,又因為Class的相關資料存盤在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全域共享的,因此靜態方法鎖相當于類的一個全域鎖,會鎖所有呼叫該方法的執行緒;
- synchronized 作用于一個物件實體時,鎖住的是所有以該物件為鎖的代碼塊,它有多個佇列,當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些執行緒存盤在不同的容器中,
Synchronized 核心組件
- Wait Set:哪些呼叫 wait 方法被阻塞的執行緒被放置在這里;
- Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中;
- Entry List:Contention List 中那些有資格成為候選資源的執行緒被移動到 Entry List 中;
- OnDeck:任意時刻,最多只有一個執行緒正在競爭鎖資源,該執行緒被成為 OnDeck;
- Owner:當前已經獲取到所資源的執行緒被稱為 Owner;
- !Owner:當前釋放鎖的執行緒,
Synchronized 實作

-
JVM 每次從佇列的尾部取出一個資料用于鎖競爭候選者(OnDeck),但是并發情況下,ContentionList 會被大量的并發執行緒進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將一部分執行緒移動到 EntryList 中作為候選競爭執行緒,
-
Owner 執行緒會在 unlock 時,將 ContentionList 中的部分執行緒遷移到 EntryList 中,并指定EntryList 中的某個執行緒為 OnDeck 執行緒(一般是最先進去的那個執行緒),
-
Owner 執行緒并不直接把鎖傳遞給 OnDeck 執行緒,而是把鎖競爭的權利交給 OnDeck,OnDeck 需要重新競爭鎖,這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM 中,也把這種選擇行為稱之為“競爭切換”,
-
OnDeck 執行緒獲取到鎖資源后會變為 Owner 執行緒,而沒有得到鎖資源的仍然停留在 EntryList中,如果 Owner 執行緒被 wait 方法阻塞,則轉移到 WaitSet 佇列中,直到某個時刻通過 notify或者 notifyAll 喚醒,會重新進去 EntryList 中,
-
處于 ContentionList、EntryList、WaitSet 中的執行緒都處于阻塞狀態,該阻塞是由作業系統來完成的(Linux 內核下采用 pthread_mutex_lock 內核函式實作的),
-
Synchronized 是非公平鎖, Synchronized 在執行緒進入 ContentionList 時,等待的執行緒會先嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對于已經進入佇列的執行緒是不公平的,還有一個不公平的事情就是自旋獲取鎖的執行緒還可能直接搶占 OnDeck 執行緒的鎖資源,
-
每個物件都有個 monitor 物件,加鎖就是在競爭 monitor 物件,代碼塊加鎖是在前后分別加上 monitorenter 和 monitorexit 指令來實作的,方法加鎖是通過一個標記位來判斷的
-
synchronized 是一個重量級操作,需要呼叫作業系統相關介面,性能是低效的,有可能給執行緒加鎖消耗的時間比有用操作消耗的時間更多,
-
Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高,在之后推出的 Java1.7 與 1.8 中,均對該關鍵字的實作機理做了優化,引入了偏向鎖和輕量級鎖,都是在物件頭中有標記位,不需要經過作業系統加鎖,
-
鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,這種升級程序叫做鎖膨脹;
-
JDK 1.6 中默認是開啟偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖,
10.執行緒中斷(interrupt)
中斷一個執行緒,其本意是給這個執行緒一個通知信號,會影響這個執行緒內部的一個中斷標識位,這 個執行緒本身并不會因此而改變狀態(如阻塞,終止等),
- 呼叫 interrupt()方法并不會中斷一個正在運行的執行緒,也就是說處于 Running 狀態的線 程并不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已,
- 若呼叫 sleep()而使執行緒處于 TIMED-WATING 狀態,這時呼叫 interrupt()方法,會拋出 InterruptedException,從而使執行緒提前結束 TIMED-WATING 狀態,
- 許多宣告拋出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),拋出異 常前,都會清除中斷標識位,所以拋出例外后,呼叫 isInterrupted()方法將會回傳 false,
- 中斷狀態是執行緒固有的一個標識位,可以通過此標識位安全的終止執行緒,比如,你想終止 一個執行緒 thread 的時候,可以呼叫 thread.interrupt()方法,在執行緒的 run 方法內部可以 根據 thread.isInterrupted()的值來優雅的終止執行緒,
引起執行緒背景關系切換的原因
- 當前執行任務的時間片用完之后,系統 CPU 正常調度下一個任務;
- 當前執行任務碰到 IO 阻塞,調度器將此任務掛起,繼續下一任務;
- 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
- 用戶代碼掛起當前任務,讓出 CPU 時間;
- 硬體中斷;
11.執行緒背景關系切換
巧妙地利用了時間片輪轉的方式, CPU 給每個任務都服務一定的時間,然后把當前任務的狀態保存下來,在加載下一任務的狀態后,繼續服務下一任務,任務的狀態保存及再加載, 這段程序就叫做背景關系切換,時間片輪轉的方式使多個任務在同一顆 CPU 上執行變成了可能,

行程:(有時候也稱做任務)是指一個程式運行的實體,在 Linux 系統中,執行緒就是能并行運行并且與他們的父行程(創建他們的行程)共享同一地址空間(一段記憶體區域)和其他資源的輕量級的行程,
背景關系: 是指某一時間點 CPU 暫存器和程式計數器的內容,
暫存器: 是 CPU 內部的數量較少但是速度很快的記憶體(與之對應的是 CPU 外部相對較慢的 RAM 主記憶體),暫存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程式運行的速度,
程式計數器: 是一個專用的暫存器,用于表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴于特定的系統,
PCB-“切換楨:背景關系切換可以認為是內核(作業系統的核心)在 CPU 上對于行程(包括執行緒)進行切換,背景關系切換程序中的資訊是保存在行程控制塊(PCB, process control block)中的,PCB 還經常被稱作“切換楨”(switchframe),資訊會一直保存到 CPU 的記憶體中,直到他們被再次使用,
背景關系切換的活動:
- 掛起一個行程,將這個行程在 CPU 中的狀態(背景關系)存盤于記憶體中的某處,
- 在記憶體中檢索下一個行程的背景關系并將其在 CPU 的暫存器中恢復,
- 跳轉到程式計數器所指向的位置(即跳轉到行程被中斷時的代碼行),以恢復該行程在程式
中,
引起執行緒背景關系切換的原因:
- 當前執行任務的時間片用完之后,系統 CPU 正常調度下一個任務;
- 當前執行任務碰到 IO 阻塞,調度器將此任務掛起,繼續下一任務;
- 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
- 用戶代碼掛起當前任務,讓出 CPU 時間;
- 硬體中斷;
12.JAVA 阻塞佇列
阻塞佇列,關鍵字是阻塞,先理解阻塞的含義,在阻塞佇列中,執行緒阻塞有這樣的兩種情況:
- 當佇列中沒有資料的情況下,消費者端的所有執行緒都會被自動阻塞(掛起),直到有資料放入佇列,
- 當佇列中填滿資料的情況下,生產者端的所有執行緒都會被自動阻塞(掛起),直到佇列中有空的位置,執行緒被自動喚醒,

Java 中的阻塞佇列
1.ArrayBlockingQueue(公平、非公平)
用陣列實作的有界阻塞佇列,此佇列按照先進先出(FIFO)的原則對元素進行排序,默認情況下不保證訪問者公平的訪問佇列,所謂公平訪問佇列是指阻塞的所有生產者執行緒或消費者執行緒,當佇列可用時,可以按照阻塞的先后順序訪問佇列,即先阻塞的生產者執行緒,可以先往佇列里插入元素,先阻塞的消費者執行緒,可以先從佇列里獲取元素,通常情況下為了保證公平性會降低吞吐量,
//我們可以使用以下代碼創建一個公平的阻塞佇列:
ArrayBlockingQueue fairQueue =
new ArrayBlockingQueue(1000,true);
2.LinkedBlockingQueue(兩個獨立鎖提高并發)
基于鏈表的阻塞佇列,同 ArrayListBlockingQueue 類似,此佇列按照先進先出(FIFO)的原則對元素進行排序,而 LinkedBlockingQueue 之所以能夠高效的處理并發資料,還因為其對于生產者端和消費者端分別采用了獨立的鎖來控制資料同步,這也意味著在高并發的情況下生產者和消費者可以并行地操作佇列中的資料,以此來提高整個佇列的并發性能,
LinkedBlockingQueue 會默認一個類似無限大小的容量
(Integer.MAX_VALUE),
3.PriorityBlockingQueue(compareTo 排序實作優先)
是一個支持優先級的無界佇列,默認情況下元素采取自然順序升序排列,可以自定義實作compareTo()方法來指定元素進行排序規則,或者初始化 PriorityBlockingQueue 時,指定構造引數 Comparator 來對元素進行排序,需要注意的是不能保證同優先級元素的順序,
4.DelayQueue(快取失效、定時任務 )
是一個支持延時獲取元素的無界阻塞佇列,佇列使用 PriorityQueue 來實作,佇列中的元素必須實作 Delayed 介面,在創建元素時可以指定多久才能從佇列中獲取當前元素,只有在延遲期滿時才能從佇列中提取元素,我們可以將 DelayQueue 運用在以下應用場景:
-
快取系統的設計:可以用 DelayQueue 保存快取元素的有效期,使用一個執行緒回圈查詢DelayQueue,一旦能從 DelayQueue 中獲取元素時,表示快取有效期到了,
-
定時任務調度:使用 DelayQueue 保存當天將會執行的任務和執行時間,一旦從DelayQueue 中獲取到任務就開始執行,從比如 TimerQueue 就是使用 DelayQueue 實作的,
5.SynchronousQueue(不存盤資料、可用于傳遞資料)
是一個不存盤元素的阻塞佇列,每一個 put 操作必須等待一個 take 操作,否則不能繼續添加元素,
SynchronousQueue 可以看成是一個傳球手,負責把生產者執行緒處理的資料直接傳遞給消費者執行緒,佇列本身并不存盤任何元素,非常適合于傳遞性場景,比如在一個執行緒中使用的資料,傳遞給
另 外 一 個 線 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和ArrayBlockingQueue,
6.LinkedTransferQueue是 一 個 由 鏈 表 結 構 組 成 的 無 界 阻 塞 TransferQueue 隊 列 ,
相 對 于 其 他 阻 塞 隊 列 ,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法,
transfer 方法:
如果當前有消費者正在等待接收元素
(消費者使用 take()方法或帶時間限制的
poll()方法,transfer 方法可以把生產者傳入的元素立刻 transfer(傳輸)給消費者,如果沒有消費者在等待接收元素,transfer 方法會將元素存放在佇列的 tail 節點,并等到該元素被消費者消費了才回傳,
tryTransfer 方法,則是用來試探下生產者傳入的元素是否能直接傳給消費者,如果沒有消費者等待接收元素,則回傳 false,和 transfer 方法的區別是 tryTransfer 方法無論消費者是否接收,方法立即回傳,而 transfer 方法是必須等到消費者消費了才回傳,
對于帶有時間限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,則是試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再回傳,如果超時還沒消費元素,則回傳 false,如果在超時時間內消費了元素,則回傳true,
7.LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞佇列,所謂雙向佇列指的你可以從佇列的兩端插入和移出元素,
雙端佇列因為多了一個操作佇列的入口,在多執行緒同時入隊時,也就減少了一半的競爭,相比其他的阻塞佇列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 單詞結尾的方法,表示插入,獲取(peek)或移除雙端佇列的第一個元素,以 Last 單詞結尾的方法,表示插入,獲取或移除雙端佇列的最后一個元素,另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst,但是 take 方法卻等同于 takeFirst,不知道是不是 Jdk 的 bug,使用時還是用帶有 First 和 Last 后綴的方法更清楚,在初始化 LinkedBlockingDeque 時可以設定容量防止其過渡膨脹,另外雙向阻塞佇列可以運用在“作業竊取”模式中,
阻塞佇列的主要方法:

添加資料的操作:
public abstract boolean add(E paramE)
將指定元素插入此佇列中(如果立即可行且不會違反容量限制),
成功時回傳 true,如果當前沒有可用的空間,
則拋出 IllegalStateException,
如果該元素是 NULL,則會拋出NullPointerException 例外,
public abstract boolean offer(E paramE)
將指定元素插入此佇列中(如果立即可行且不會違反容量限制),
成功時回傳 true,如果當前沒有可用的空間,則回傳 false,
public abstract void put(E paramE) throws InterruptedException
將指定元素插入此佇列中,將等待可用的空間(如果有必要)
offer(E o, long timeout, TimeUnit unit)
可以設定等待的時間,如果在指定的時間內,
還不能往佇列中加入 BlockingQueue,則回傳失敗,
獲取資料操作:
poll(time):取走 BlockingQueue 里排在首位的物件,
若不能立即取出,則可以等 time 引數規定的時間,取不到時回傳 null;
poll(long timeout, TimeUnit unit)
從 BlockingQueue 取出一個隊首的物件,如果在指定時間內,
佇列一旦有資料可取,則立即回傳佇列中的資料,
否則直到時間超時還沒有資料可取,回傳失敗,
take()
取走 BlockingQueue 里排在首位的物件,若 BlockingQueue 為空,
阻斷進入等待狀態直到 BlockingQueue 有新的資料被加入,
drainTo()
一次性從 BlockingQueue 獲取所有可用的資料物件
(還可以指定獲取資料的個數),
通過該方法,可以提升獲取資料效率;不需要多次分批加鎖或釋放鎖,
13.CyclicBarrier、CountDownLatch、Semaphore ,Exchanger的用法
CyclicBarrier、CountDownLatch、Semaphore 的用法
Exchanger用于兩個執行緒之間交換資料,
public class ExchangerTest {
public static final Exchanger<String> ex=new Exchanger<>();
private static ExecutorService pool= Executors.newFixedThreadPool(2);
public static void main(String[] args) {
pool.execute(()->{
String A="銀行流水A";
try {
ex.exchange(A);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
pool.execute(()->{
String B="銀行流水B";
try {
String A=ex.exchange(B);
System.out.println("AB錄入的是否一致"+A.equals(B)+
"A:"+A+"B"+B);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
14.LockSupport
是一個JUC中的類用于創建鎖和其他同步類的基本執行緒阻塞原語,
LockSupport類使用了一 種名為Permit (許可)的概念來做到阻塞和喚醒執行緒的功能, 每個執行緒都有一個許可(permit)permit只有兩個值1和等, 默認是零,
可以把許可看成是一種(0,1)信號量( Semaphore) ,但與Semaphore不同的是,許可的累加上限是1.
//permit默認是0,所以一開始呼叫park()方法, 當前執行緒就會阻塞
//直到別的執行緒將當前執行緒的permit設定為1時,park方法會被喚醒,
//然后會將permit再次設定為0并回傳,
public static void park() {
U.park(false, 0L);
}
//呼叫unpark(hread)方法后,就會將thread執行緒的許可permit設定成1
//(注意多次呼叫unpark方法,不會累加,permit值還是1)會自動喚醒
//thread執行緒,即之前阻塞中的LockSupport.park(方法會立即回傳,
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
LockSupport不需要鎖支持,可以先喚醒后等待
public class TestLockSupport {
public static void main(String[] args) {
Thread a=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t執行緒進入了");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t執行緒被喚醒了");
},"AA");
a.start();
Thread b=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t執行緒發出了喚醒通知");
LockSupport.unpark(a);
},"BB");
b.start();
}
}
為什么可以先喚醒執行緒后阻塞執行緒?
因為unpark獲得了一個憑證,之后再呼叫park方法,就可以名正言順的憑證消費,故不會阻塞,
為什么喚醒兩次后阻塞兩次,但最終結果還會阻塞執行緒?
因為憑證的數量最多為1,連續呼叫兩次unpark和呼叫一次unpark效果一樣,只會增加一個憑證;而呼叫兩次park卻需要消費兩個憑證,證不夠,不能放行,
15.Java并發容器有哪些?
ConcurrentHashMap CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentLinkedQueue
ConcurrentLinkedDeque ConcurrentSkipListMap
ConcurrentSkipListSetArrayBlockingQueue
LinkedBlockingQueue LinkedBlockingDeque
PriorityBlockingQueue
SynchronousQueue
LinkedTransferQueue DelayQueue
16.什么是寫時復制
Copyonwrite容器即寫時復制的容器,往一個容器添加元 素的時候,不直接往當前容器Object[]添加,而是先將當前容器object[ ]進行copy,復制出一個新的容器0bject[] newElements, 然后新的容器0bject[] newElements 里添加元素,添加完元素之后,再將原容器的參考指向新的容器setArray(newElements);. 這樣做的好處是可以對CopyOnwrite容器進行并發的讀,而不需要加鎖,因為當前容器不會添加任何元素,所以CopyonWrite 容器也是一“種讀寫分離的思想, 讀和寫不同的容器
public boolean add(E e)
{
final Reentrantlock lock = this. lock;
lock. lock();
try{
object[] ele ments = getArray();
int Len = elements. length;
object[] newElements = Arrays. copyof(elements, len + 1);
newELements[len] = e;
setArray(newElements );
return true;
}finally {
lock. unlock();
}
}
17.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
執行緒類的構造方法、靜態塊是被 new這個執行緒類所在的執行緒所呼叫的,而 run 方法里面的代碼才是被執行緒自身所呼叫的,
舉個例子,假設 Thread2 中 new 了Thread1,main 函式中 new 了 Thread2,那么:
1、Thread2 的構造方法、靜態塊是 main 執行緒呼叫的,Thread2 的 run()方法是Thread2 自己呼叫的.
2、Thread1 的構造方法、靜態塊是 Thread2 呼叫的,Thread1 的 run()方法是Thread1 自己呼叫的.
18.什么是 AQS(抽象的佇列同步器)

AbstractQueuedSynchronizer 類如其名,抽象的佇列式的同步器,AQS 定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實作都依賴于它,如常用的
ReentrantLock/Semaphore/CountDownLatch,

它維護了一個 volatile int state(代表共享資源)和一個 FIFO 執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列),這里 volatile 是核心關鍵詞,具體 volatile 的語意,在此不述,state 的訪問方式有三種:
getState()
setState()
compareAndSetState()
AQS 定義兩種資源共享方式
Exclusive 獨占資源-ReentrantLock
Exclusive(獨占,只有一個執行緒能執行,如 ReentrantLock)
Share 共享資源-Semaphore/CountDownLatch
Share(共享,多個執行緒可同時執行,如 Semaphore/CountDownLatch),
AQS 只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實作,AQS 這里只定義了一個介面,具體資源的獲取交由自定義同步器去實作了(通過 state 的 get/set/CAS)之所以沒有定義成abstract ,是 因 為獨 占模 式 下 只 用實作 tryAcquire-tryRelease ,而 共享 模 式 下 只用 實 現
tryAcquireShared-tryReleaseShared,如果都定義成abstract,那么每個模式也要去實作另一模式下的介面,不同的自定義同步器爭用共享資源的方式也不同,自定義同步器在實作時只需要實作共享資源 state 的獲取與釋放方式即可,至于具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS 已經在頂層實作好了,自定義同步器實作時
主要實作以下幾種方法:
- isHeldExclusively():該執行緒是否正在獨占資源,只有用到 condition 才需要去實作它,
- tryAcquire(int):獨占方式,嘗試獲取資源,成功則回傳 true,失敗則回傳 false,
- tryRelease(int):獨占方式,嘗試釋放資源,成功則回傳 true,失敗則回傳 false,
- tryAcquireShared(int):共享方式,嘗試獲取資源,負數表示失敗;0 表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源,
- tryReleaseShared(int):共享方式,嘗試釋放資源,如果釋放后允許喚醒后續等待結點回傳true,否則回傳 false,
同步器的實作是 ABS 核心(state 資源狀態計數)
同步器的實作是 ABS 核心,以 ReentrantLock 為例,state 初始化為 0,表示未鎖定狀態,A 執行緒lock()時,會呼叫 tryAcquire()獨占該鎖并將 state+1,此后,其他執行緒再 tryAcquire()時就會失敗,直到 A 執行緒 unlock()到 state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖,當然,釋放鎖之前,A 執行緒自己是可以重復獲取此鎖的(state 會累加),這就是可重入的概念,但要注意,獲取多少次就要釋放多么次,這樣才能保證 state 是能回到零態的,
以 CountDownLatch 以例,任務分為 N 個子執行緒去執行,state 也初始化為 N(注意 N 要與執行緒個數一致),這 N 個子執行緒是并行執行的,每個子執行緒執行完后 countDown()一次,state會 CAS 減 1,等到所有子執行緒都執行完后(即 state=0),會 unpark()主呼叫執行緒,然后主呼叫執行緒就會從 await()函式回傳,繼續后余動作,
ReentrantReadWriteLock 實作獨占和共享兩種方式
一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實作 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一種即可,但 AQS 也支持自定義同步器
同時實作獨占和共享兩種方式,如 ReentrantReadWriteLock,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/299975.html
標籤:java
