💂 個人主頁: Java程式魚
🤟 整個Java 體系的面試題我都會分享,大家可以持續關注
💬 如果文章對你有幫助、歡迎關注、點贊、收藏(一鍵三連)和訂閱專欄哦,
💅 有任何問題歡迎私信,看到會及時回復!
| 序號 | 內容 | 鏈接地址 |
|---|---|---|
| 1 | Java基礎知識面試題 | https://blog.csdn.net/qq_35620342/article/details/119636436 |
| 2 | Java集合容器面試題 | 待分享 |
| 3 | Java并發編程面試題 | 待分享 |
| 4 | Java例外面試題 | 待分享 |
| 5 | JVM面試題 | 待分享 |
| 6 | Java Web面試題 | https://blog.csdn.net/qq_35620342/article/details/119642114 |
| 7 | Spring面試題 | https://blog.csdn.net/qq_35620342/article/details/119956512 |
| 8 | Spring MVC面試題 | https://blog.csdn.net/qq_35620342/article/details/119965560 |
| 9 | Spring Boot面試題 | 待分享 |
| 10 | MyBatis面試題 | https://blog.csdn.net/qq_35620342/article/details/119956541 |
| 11 | Spring Cloud面試題 | 待分享 |
| 12 | Redis面試題 | https://blog.csdn.net/qq_35620342/article/details/119575020 |
| 13 | MySQL資料庫面試題 | https://blog.csdn.net/qq_35620342/article/details/119930887 |
| 14 | RabbitMQ面試題 | 待分享 |
| 15 | Dubbo面試題 | 待分享 |
| 16 | Linux面試題 | 待分享 |
| 17 | Tomcat面試題 | 待分享 |
| 18 | ZooKeeper面試題 | 待分享 |
| 19 | Netty面試題 | 待分享 |
| 20 | 資料結構與演算法面試題 | 待分享 |
文章目錄
- 1.為什么要使用多執行緒?
- 2.多執行緒應用場景
- 3.并發編程有什么缺點?
- 4.并發編程三個必要因素是什么?
- 5.在 Java 程式中怎么保證多執行緒的運行安全?
- 6.什么是多執行緒?
- 7.多執行緒的好處
- 8.多執行緒的劣勢
- 9.什么是背景關系切換?
- 10.守護執行緒和用戶執行緒有什么區別呢?
- 11.如何在 Windows 和 Linux 上查找哪個執行緒cpu利用率最高?
- 12.什么是執行緒死鎖
- 13.形成死鎖的四個必要條件是什么?
- 14.如何避免執行緒死鎖?
- 15.創建執行緒有哪幾種方式?
- 16.說一下 runnable 和 callable 有什么區別
- 17.執行緒有哪些狀態?
- 18.sleep() 和 wait() 有什么區別?
- 19.notify()和 notifyAll()有什么區別?
- 20.執行緒的 run()和 start()有什么區別?
- 21.為什么執行緒通信的方法 wait(), notify()和 notifyAll()被定義在 Object 類里?
- 22.為什么 wait(), notify()和 notifyAll()必須在同步方法或者同步塊中被呼叫?
- 23.創建多少個執行緒合適?
- 24.什么是執行緒池?
- 25.創建執行緒池有哪幾種方式?
- 26.執行緒池作業原理
- 27.執行緒池有什么作用?
- 28.執行緒池有什么優點?
- 29.什么是阻塞式方法?
- 30.Java 中你怎樣喚醒一個阻塞的執行緒?
- 31.如何在兩個執行緒間共享資料?
- 32.Java 如何實作多執行緒之間的通訊和協作?
- 33.同步方法和同步塊,哪個是更好的選擇?
- 34.什么是執行緒同步和執行緒互斥,有哪幾種實作方式?
- 35.在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?
- 36.如果你提交任務時,執行緒池佇列已滿,這時會發生什么
- 37.什么叫執行緒安全?servlet 是執行緒安全嗎?
- 38.在 Java 程式中怎么保證多執行緒的運行安全?
- 39.你對執行緒優先級的理解是什么?
- 40.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
- 41.Java 中怎么獲取一份執行緒 dump 檔案?你如何在 Java 中獲取執行緒堆疊?
- 42.一個執行緒運行時發生例外會怎樣?
- 43.Java 執行緒數過多會造成什么例外?
- 44.多執行緒的常用方法
- 45.Java中垃圾回收有什么目的?什么時候進行垃圾回收?
- 46.執行緒之間如何通信及執行緒之間如何同步?
- 47.Java記憶體模型
- 48.如果物件的參考被置為null,垃圾收集器是否會立即釋放物件占用的記憶體?
- 49.finalize()方法什么時候被呼叫?解構式(finalization)的目的是什么?
- 50.什么是重排序?
- 51.重排序實際執行的指令步驟
- 52.重排序遵守的規則
- 53.as-if-serial規則和happens-before規則的區別
- 54.并發關鍵字 synchronized
- 55.說說自己是怎么使用 synchronized 關鍵字,在專案中用到了嗎
- 56.單例模式了解嗎?給我解釋一下雙重檢驗鎖方式實作單例模式
- 57.說一下 synchronized 底層實作原理?
- 58.synchronized可重入的原理
- 59.什么是自旋
- 60.多執行緒中 synchronized 鎖升級的原理是什么?
- 61.執行緒 B 怎么知道執行緒 A 修改了變數
- 62.當一個執行緒進入一個物件的 synchronized 方法 A 之后,其它執行緒是否可進入此物件的 synchronized 方法 B?
- 63.synchronized、volatile、CAS 比較
- 64.synchronized 和 Lock 有什么區別?
- 65.synchronized 和 ReentrantLock 區別是什么?
- 66.volatile 關鍵字的作用
- 67.Java 中能創建 volatile 陣列嗎?
- 68.volatile 變數和 atomic 變數有什么不同?
- 69.volatile 能使得一個非原子操作變成原子操作嗎?
- 70.synchronized 和 volatile 的區別是什么?
- 71.final不可變物件,它對寫并發應用有什么幫助?
- 72.Lock 介面和synchronized 對比同步它有什么優勢?
- 73.樂觀鎖和悲觀鎖的理解及如何實作,有哪些實作方式?
- 74.什么是 CAS
- 75.CAS 的會產生什么問題?
- 76.什么是原子類
- 77.原子類的常用類
- 78.說一下 Atomic的原理?
- 79.死鎖與活鎖的區別,死鎖與饑餓的區別?
1.為什么要使用多執行緒?
使用多執行緒,本質上就是提升程式性能,不過此刻談到的性能,可能在你腦海里還是比較籠統的,基本上就是快、快、快,這種無法度量的感性認識很不科學,所以在提升性能之前,首要問題是:如何度量性能,
度量性能的指標有很多,但是有兩個指標是最核心的,它們就是延遲和吞吐量,延遲指的是發出請求到收到回應這個程序的時間;延遲越短,意味著程式執行得越快,性能也就越好, 吞吐量指的是在單位時間內能處理請求的數量;吞吐量越大,意味著程式能處理的請求越多,性能也就越好,這兩個指標內部有一定的聯系(同等條件下,延遲越短,吞吐量越大),但是由于它們隸屬不同的維度(一個是時間維度,一個是空間維度),并不能互相轉換,
我們所謂提升性能,從度量的角度,主要是降低延遲,提高吞吐量,這也是我們使用多執行緒的主要目的,那我們該怎么降低延遲,提高吞吐量呢?這個就要從多執行緒的應用場景說起了,
2.多執行緒應用場景
- 多執行緒處理后臺任務,例如定時向100萬用戶發送郵件
- 異步處理,例如:發微博、記錄日志等
- 多執行緒分布式計算
要想“降低延遲,提高吞吐量”,對應的方法呢,基本上有兩個方向,一個方向是優化演算法,另一個方向是將硬體的性能發揮到極致,前者屬于演算法范疇,后者則是和并發編程息息相關了,那計算機主要有哪些硬體呢?主要是兩類:一個是 I/O,一個是 CPU,簡言之,在并發編程領域,提升性能本質上就是提升硬體的利用率,再具體點來說,就是提升 I/O 的利用率和 CPU 的利用率,
估計這個時候你會有個疑問,作業系統不是已經解決了硬體的利用率問題了嗎?的確是這樣,例如作業系統已經解決了磁盤和網卡的利用率問題,利用中斷機制還能避免 CPU 輪詢 I/O 狀態,也提升了 CPU 的利用率,但是作業系統解決硬體利用率問題的物件往往是單一的硬體設備,而我們的并發程式,往往需要 CPU 和 I/O 設備相互配合作業,也就是說,我們需要解決 CPU 和 I/O 設備綜合利用率的問題,關于這個綜合利用率的問題,作業系統雖然沒有辦法完美解決,但是卻給我們提供了方案,那就是:多執行緒,
下面我們用一個簡單的示例來說明:如何利用多執行緒來提升 CPU 和 I/O 設備的利用率?假設程式按照 CPU 計算和 I/O 操作交叉執行的方式運行,而且 CPU 計算和 I/O 操作的耗時是 1:1,
如下圖所示,如果只有一個執行緒,執行 CPU 計算的時候,I/O 設備空閑;執行 I/O 操作的時候,CPU 空閑,所以 CPU 的利用率和 I/O 設備的利用率都是 50%,

如果有兩個執行緒,如下圖所示,當執行緒 A 執行 CPU 計算的時候,執行緒 B 執行 I/O 操作;當執行緒 A 執行 I/O 操作的時候,執行緒 B 執行 CPU 計算,這樣 CPU 的利用率和 I/O 設備的利用率就都達到了 100%,

我們將 CPU 的利用率和 I/O 設備的利用率都提升到了 100%,會對性能產生了哪些影響呢?通過上面的圖示,很容易看出:單位時間處理的請求數量翻了一番,也就是說吞吐量提高了 1 倍,此時可以逆向思維一下,如果 CPU 和 I/O 設備的利用率都很低,那么可以嘗試通過增加執行緒來提高吞吐量,
在單核時代,多執行緒主要就是用來平衡 CPU 和 I/O 設備的,如果程式只有 CPU 計算,而沒有 I/O 操作的話,多執行緒不但不會提升性能,還會使性能變得更差,原因是增加了執行緒切換的成本,但是在多核時代,這種純計算型的程式也可以利用多執行緒來提升性能,為什么呢?因為利用多核可以降低回應時間,
為便于你理解,這里我舉個簡單的例子說明一下:計算 1+2+… … +100 億的值,如果在 4 核的 CPU 上利用 4 個執行緒執行,執行緒 A 計算[1,25 億),執行緒 B 計算[25 億,50 億),執行緒 C 計算[50,75 億),執行緒 D 計算[75 億,100 億],之后匯總,那么理論上應該比一個執行緒計算[1,100 億]快將近 4 倍,回應時間能夠降到 25%,一個執行緒,對于 4 核的 CPU,CPU 的利用率只有 25%,而 4 個執行緒,則能夠將 CPU 的利用率提高到 100%,

3.并發編程有什么缺點?
- 并發編程的目的就是為了能提高程式的執行效率,提高程式運行速度,但是并發編程并不總是能提高程式運行速度的,而且并發編程可能會遇到很多問題,比如:記憶體泄漏、背景關系切換、執行緒安全、死鎖等問題,
4.并發編程三個必要因素是什么?
- 原子性:原子,即一個不可再被分割的顆粒,原子性指的是一個或多個操作要么全部執行成功要么全部執行失敗,
- 可見性:一個執行緒對共享變數的修改,另一個執行緒能夠立刻看到,
- 有序性:程式執行的順序按照代碼的先后順序執行,(處理器可能會對指令進行重排序)
5.在 Java 程式中怎么保證多執行緒的運行安全?
-
出現執行緒安全問題的原因一般都是三個原因:
-
執行緒切換帶來的原子性問題
解決辦法:使用多執行緒之間同步synchronized或使用鎖(lock), -
CPU快取導致的可見性問題
解決辦法:synchronized、volatile、LOCK,可以解決可見性問題 -
編譯優化帶來的有序性問題
解決辦法:Happens-Before 規則可以解決有序性問題
-
6.什么是多執行緒?
在介紹多執行緒之前,我先給大家講解下行程,
什么是行程?
電腦中時會有很多單獨運行的程式,每個程式有一個獨立的行程,而行程之間是相互獨立存在的,比如下圖中的QQ、酷狗播放器、電腦管家等等,

什么是執行緒?
行程想要執行任務就需要依賴執行緒,換句話說,就是行程中的最小執行單位就是執行緒,并且一個行程中至少有一個執行緒,
那什么是多執行緒呢?
提到多執行緒這里要說兩個概念,就是串行和并行,搞清楚這個,我們才能更好地理解多執行緒,
所謂串行,其實是相對于單條執行緒來執行多個任務來說的,我們就拿下載檔案來舉個例子:當我們下載多個檔案時,在串行中它是按照一定的順序去進行下載的,也就是說,必須等下載完A之后才能開始下載B,它們在時間上是不可能發生重疊的,

并行:下載多個檔案,開啟多條執行緒,多個檔案同時進行下載,這里是嚴格意義上的,在同一時刻發生的,并行在時間上是重疊的,

了解了這兩個概念之后,我們再來說說什么是多執行緒,舉個例子,我們打開騰訊管家,騰訊管家本身就是一個程式,也就是說它就是一個行程,它里面有很多的功能,我們可以看下圖,能查殺病毒、清理垃圾、電腦加速等眾多功能,
按照單執行緒來說,無論你想要清理垃圾、還是要病毒查殺,那么你必須先做完其中的一件事,才能做下一件事,這里面是有一個執行順序的,
如果是多執行緒的話,我們其實在清理垃圾的時候,還可以進行查殺病毒、電腦加速等等其他的操作,這個是嚴格意義上的同一時刻發生的,沒有執行上的先后順序,

以上就是,一個行程運行時產生了多個執行緒,
在了解完這個問題后,我們又需要去了解一個使用多執行緒不得不考慮的問題——執行緒安全,
今天我們不說如何保證一個執行緒的安全,我們聊聊什么是執行緒安全?因為我之前面試被問到了,說真的,我之前真的不是特別了解這個問題,我們好像只學了如何確保一個執行緒安全,卻不知道所謂的安全到底是什么!
并行和并發的區別:
- 并發,指的是多個事情,在同一時間段內同時發生了,
- 并行,指的是多個事情,在同一時間點上同時發生了,
并發的多個任務之間是互相搶占資源的,
并行的多個任務之間是不互相搶占資源的、
只有在多CPU或者一個CPU多核的情況中,才會發生并行,否則,看似同時發生的事情,其實都是并發執行的,
7.多執行緒的好處
- 可以提高 CPU 的利用率,在多執行緒程式中,一個執行緒必須等待的時候,CPU 可以運行其它的執行緒而不是等待,這樣就大大提高了程式的效率,也就是說允許單個程式創建多個并行執行的執行緒來完成各自的任務,
8.多執行緒的劣勢
-
執行緒也是程式,所以執行緒需要占用記憶體,執行緒越多占用記憶體也越多;
-
多執行緒需要協調和管理,所以需要 CPU 時間跟蹤執行緒;
-
執行緒之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題,
9.什么是背景關系切換?
-
多執行緒編程中一般執行緒的個數都大于 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU 采取的策略是為每個執行緒分配時間片并輪轉的形式,當一個執行緒的時間片用完的時候就會重新處于就緒狀態讓給其他執行緒使用,這個程序就屬于一次背景關系切換,
-
概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態,任務從保存到再加載的程序就是一次背景關系切換,
-
背景關系切換通常是計算密集型的,也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間,所以,背景關系切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作,
-
Linux 相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其背景關系切換和模式切換的時間消耗非常少,
減少背景關系切換的方法:
①無鎖并發編程,多執行緒競爭時,會引起背景關系切換,所以多執行緒處理資料時,可以用一些辦法來避免使用鎖,如將資料的ID按照Hash取模分段,不同的執行緒處理不同段的資料,
②CAS演算法,Java的Atomic包使用CAS演算法來更新資料,而不需要加鎖,
③使用最少執行緒,避免創建不需要的執行緒,比如任務很少,但是創建了很多執行緒來處理,這樣會造成大量執行緒都處于等待狀態
④協程,在單執行緒里實作多任務的調度,并在單執行緒里維持多個任務間的切換
10.守護執行緒和用戶執行緒有什么區別呢?
- 用戶 (User) 執行緒:用戶執行緒和守護執行緒唯一的區別就是Daemon(Thread.getDaemon())為false;
- 守護 (Daemon) 執行緒:為程式提供后端服務的執行緒成為守護執行緒,非守護執行緒運行結束時守護執行緒也一并結束;
栗子:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("守護執行緒開始執行...");
while(true){
System.out.println("守護執行緒休眠中..");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setDaemon(true);
thread.start();
try {
System.out.println(Thread.currentThread().getName() + "執行緒已休眠..");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "執行緒休眠結束..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
執行結果:
main執行緒已休眠..
守護執行緒開始執行...
守護執行緒休眠中..
守護執行緒休眠中..
守護執行緒休眠中..
守護執行緒休眠中..
守護執行緒休眠中..
守護執行緒休眠中..
執行緒休眠結束..
從列印資訊上可以看出我們設定的守護執行緒為while(true)每隔一秒列印一次資訊,main執行緒休眠5秒后結束,main執行緒結束時jvm直接停止了;
11.如何在 Windows 和 Linux 上查找哪個執行緒cpu利用率最高?
- windows上面用任務管理器看,linux下可以用 top 這個工具看,
- 找出cpu耗用厲害的行程pid, 終端執行top命令,然后按下shift+p (shift+m是找出消耗記憶體最高)查找出cpu利用最厲害的pid號
- 根據上面第一步拿到的pid號,top -H -p pid ,然后按下shift+p,查找出cpu利用率最厲害的執行緒號,比如top -H -p 1328
- 將獲取到的執行緒號轉換成16進制,去百度轉換一下就行
- 使用jstack工具將行程資訊列印輸出,jstack pid號 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat
- 編輯/tmp/t.dat檔案,查找執行緒號對應的資訊
或者直接使用JDK自帶的工具查看“jconsole” 、“visualVm”,這都是JDK自帶的,可以直接在JDK的bin目錄下找到直接使用
12.什么是執行緒死鎖
- 死鎖是指兩個或兩個以上的行程(執行緒)在執行程序中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去,此時稱系統處于死鎖狀態或系統產生了死鎖,這些永遠在互相等待的行程(執行緒)稱為死鎖行程(執行緒),
- 多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放,由于執行緒被無限期地阻塞,因此程式不可能正常終止,
- 如下圖所示,執行緒 A 持有資源 2,執行緒 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個執行緒就會互相等待而進入死鎖狀態,

13.形成死鎖的四個必要條件是什么?
- 互斥條件:在一段時間內某資源只由一個行程占用,如果此時還有其它行程請求資源,就只能等待,直至占有資源的行程用畢釋放,
- 占有且等待條件:指行程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它行程占有,此時請求行程阻塞,但又對自己已獲得的其它資源保持不放,
- 不可搶占條件:別人已經占有了某項資源,你不能因為自己也需要該資源,就去把別人的資源搶過來,
- 回圈等待條件:若干行程之間形成一種頭尾相接的回圈等待資源關系,(比如一個行程集合,A在等B,B在等C,C在等A)
14.如何避免執行緒死鎖?
- 避免一個執行緒同時獲得多個鎖
- 避免一個執行緒在鎖內同時占用多個資源,盡量保證每個鎖只占用一個資源
- 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制
15.創建執行緒有哪幾種方式?
(1)繼承Thread類創建執行緒類
-
定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務,因此把run()方法稱為執行體,
-
創建Thread子類的實體,即創建了執行緒物件,
-
呼叫執行緒物件的start()方法來啟動該執行緒,
(2)通過Runnable介面創建執行緒類
-
定義runnable介面的實作類,并重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體,
-
創建 Runnable實作類的實體,并依此實體作為Thread的target來創建Thread物件,該Thread物件才是真正的執行緒物件,
-
呼叫執行緒物件的start()方法來啟動該執行緒,
(3)通過Callable和Future創建執行緒
-
創建Callable介面的實作類,并實作call()方法,該call()方法將作為執行緒執行體,并且有回傳值,
-
創建Callable實作類的實體,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的回傳值,
-
使用FutureTask物件作為Thread物件的target創建并啟動新執行緒,
-
呼叫FutureTask物件的get()方法來獲得子執行緒執行結束后的回傳值,
16.說一下 runnable 和 callable 有什么區別
有點深的問題了,也看出一個Java程式員學習知識的廣度,
Runnable介面中的run()方法的回傳值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;
Callable介面中的call()方法是有回傳值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果,
17.執行緒有哪些狀態?
Java 語言中執行緒共有六種狀態,分別是:
NEW(初始化狀態)
RUNNABLE(可運行 / 運行狀態)
BLOCKED(阻塞狀態)
WAITING(無時限等待)
TIMED_WAITING(有時限等待)
TERMINATED(終止狀態)
這看上去挺復雜的,狀態型別也比較多,但其實在作業系統層面,Java 執行緒中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即前面我們提到的休眠狀態,也就是說只要 Java 執行緒處于這三種狀態之一,那么這個執行緒就永遠沒有 CPU 的使用權,所以 Java 執行緒的生命周期可以簡化為下圖:

其中,BLOCKED、WAITING、TIMED_WAITING 可以理解為執行緒導致休眠狀態的三種原因,那具體是哪些情形會導致執行緒從 RUNNABLE 狀態轉換到這三種狀態呢?而這三種狀態又是何時轉換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?
(1)RUNNABLE 與 BLOCKED 的狀態轉換
只有一種場景會觸發這種轉換,就是執行緒等待 synchronized 的隱式鎖,synchronized 修飾的方法、代碼塊同一時刻只允許一個執行緒執行,其他執行緒只能等待,這種情況下,等待的執行緒就會從 RUNNABLE 轉換到 BLOCKED 狀態,而當等待的執行緒獲得 synchronized 隱式鎖時,就又會從 BLOCKED 轉換到 RUNNABLE 狀態,
如果你熟悉作業系統執行緒的生命周期的話,可能會有個疑問:執行緒呼叫阻塞式 API 時,是否會轉換到 BLOCKED 狀態呢?在作業系統層面,執行緒是會轉換到休眠狀態的,但是在 JVM 層面,Java 執行緒的狀態不會發生變化,也就是說 Java 執行緒的狀態會依然保持 RUNNABLE 狀態,JVM 層面并不關心作業系統調度相關的狀態,因為在 JVM 看來,等待 CPU 使用權(作業系統層面此時處于可執行狀態)與等待 I/O(作業系統層面此時處于休眠狀態)沒有區別,都是在等待某個資源,所以都歸入了 RUNNABLE 狀態,
而我們平時所謂的 Java 在呼叫阻塞式 API 時,執行緒會阻塞,指的是作業系統執行緒的狀態,并不是 Java 執行緒的狀態,
(2)RUNNABLE 與 WAITING 的狀態轉換
總體來說,有三種場景會觸發這種轉換,
第一種場景,獲得 synchronized 隱式鎖的執行緒,呼叫無引數的 Object.wait() 方法,
第二種場景,呼叫無引數的 Thread.join() 方法,其中的 join() 是一種執行緒同步方法,例如有一個執行緒物件 thread A,當呼叫 A.join() 的時候,執行這條陳述句的執行緒會等待 thread A 執行完,而等待中的這個執行緒,其狀態會從 RUNNABLE 轉換到 WAITING,當執行緒 thread A 執行完,原來等待它的執行緒又會從 WAITING 狀態轉換到 RUNNABLE,
第三種場景,呼叫 LockSupport.park() 方法,其中的 LockSupport 物件,也許你有點陌生,其實 Java 并發包中的鎖,都是基于它實作的,呼叫 LockSupport.park() 方法,當前執行緒會阻塞,執行緒的狀態會從 RUNNABLE
(3)RUNNABLE 與 TIMED_WAITING 的狀態轉換
有五種場景會觸發這種轉換:
呼叫帶超時引數的 Thread.sleep(long millis) 方法;
獲得 synchronized 隱式鎖的執行緒,呼叫帶超時引數的 Object.wait(long timeout) 方法;
呼叫帶超時引數的 Thread.join(long millis) 方法;
呼叫帶超時引數的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
呼叫帶超時引數的 LockSupport.parkUntil(long deadline) 方法,
這里你會發現 TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了超時引數,
(4)從 NEW 到 RUNNABLE 狀態
Java 剛創建出來的 Thread 物件就是 NEW 狀態,而創建 Thread 物件主要有兩種方法,一種是繼承 Thread 物件,重寫 run() 方法,示例代碼如下:
// 自定義執行緒物件
class MyThread extends Thread {
public void run() {
// 執行緒需要執行的代碼
......
}
}
// 創建執行緒物件
MyThread myThread = new MyThread();
另一種是實作 Runnable 介面,重寫 run() 方法,并將該實作類作為創建 Thread 物件的引數,示例代碼如下:
// 實作Runnable介面
class Runner implements Runnable {
@Override
public void run() {
// 執行緒需要執行的代碼
......
}
}
// 創建執行緒物件
Thread thread = new Thread(new Runner());
NEW 狀態的執行緒,不會被作業系統調度,因此不會執行,Java 執行緒要執行,就必須轉換到 RUNNABLE 狀態,從 NEW 狀態轉換到 RUNNABLE 狀態很簡單,只要呼叫執行緒物件的 start() 方法就可以了,示例代碼如下:
MyThread myThread = new MyThread();
// 從NEW狀態轉換到RUNNABLE狀態
myThread.start();
實作的方式優于繼承的方式:
①避免了Java單繼承的局限性
②如果多個執行緒要操作同一份資源(或資料),更適合使用實作的方式
(5)從 RUNNABLE 到 TERMINATED 狀態
執行緒執行完 run() 方法后,會自動轉換到 TERMINATED 狀態,當然如果執行 run() 方法的時候例外拋出,也會導致執行緒終止,有時候我們需要強制中斷 run() 方法的執行,例如 run() 方法訪問一個很慢的網路,我們等不下去了,想終止怎么辦呢?Java 的 Thread 類里面倒是有個 stop() 方法,不過已經標記為 @Deprecated,所以不建議使用了,正確的姿勢其實是呼叫 interrupt() 方法,
那 stop() 和 interrupt() 方法的主要區別是什么呢?
stop() 方法會真的殺死執行緒,不給執行緒喘息的機會,如果執行緒持有 ReentrantLock 鎖,被 stop() 的執行緒并不會自動呼叫 ReentrantLock 的 unlock() 去釋放鎖,那其他執行緒就再也沒機會獲得 ReentrantLock 鎖,這實在是太危險了,所以該方法就不建議使用了,類似的方法還有 suspend() 和 resume() 方法,這兩個方法同樣也都不建議使用了,所以這里也就不多介紹了,
而 interrupt() 方法就溫柔多了,interrupt() 方法僅僅是通知執行緒,執行緒有機會執行一些后續操作,同時也可以無視這個通知,被 interrupt 的執行緒,是怎么收到通知的呢?一種是例外,另一種是主動檢測,
當執行緒 A 處于 RUNNABLE 狀態時,并且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 會觸發 java.nio.channels.ClosedByInterruptException 這個例外;而阻塞在 java.nio.channels.Selector 上時,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 的 java.nio.channels.Selector 會立即回傳,
上面這兩種情況屬于被中斷的執行緒通過例外的方式獲得了通知,還有一種是主動檢測,如果執行緒處于 RUNNABLE 狀態,并且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的執行緒 A,這時就得依賴執行緒 A 主動檢測中斷狀態了,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,那么執行緒 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了,
18.sleep() 和 wait() 有什么區別?
sleep():方法是執行緒類(Thread)的靜態方法,讓呼叫執行緒進入睡眠狀態,讓出執行機會給其他執行緒,等到休眠時間結束后,執行緒進入就緒狀態和其他執行緒一起競爭cpu的執行時間,因為sleep() 是static靜態的方法,他不能改變物件的機鎖,當一個synchronized塊中呼叫了sleep() 方法,執行緒雖然進入休眠,但是物件的機鎖沒有被釋放,其他執行緒依然無法訪問這個物件,
wait():wait()是Object類的方法,當一個執行緒執行到wait方法時,它就進入到一個和該物件相關的等待池,同時釋放物件的機鎖,使得其他執行緒能夠訪問,可以通過notify,notifyAll方法來喚醒等待的執行緒
19.notify()和 notifyAll()有什么區別?
-
如果執行緒呼叫了物件的 wait()方法,那么執行緒便會處于該物件的等待池中,等待池中的執行緒不會去競爭該物件的鎖,
-
當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖,也就是說,呼叫了notify后只要一個執行緒會由等待池進入鎖池,而notifyAll會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭,
-
優先級高的執行緒競爭到物件鎖的概率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中,而競爭到物件鎖的執行緒則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該物件鎖,這時鎖池中的執行緒會繼續競爭該物件鎖,
20.執行緒的 run()和 start()有什么區別?
每個執行緒都是通過某個特定Thread物件所對應的方法run()來完成其操作的,方法run()稱為執行緒體,通過呼叫Thread類的start()方法來啟動一個執行緒,
start()方法來啟動一個執行緒,真正實作了多執行緒運行,這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼; 這時此執行緒是處于就緒狀態, 并沒有運行, 然后通過此Thread類呼叫方法run()來完成其運行狀態, 這里方法run()稱為執行緒體,它包含了要執行的這個執行緒的內容, Run方法運行結束, 此執行緒終止,然后CPU再調度其它執行緒,
run()方法是在本執行緒里的,只是執行緒里的一個函式,而不是多執行緒的, 如果直接呼叫run(),其實就相當于是呼叫了一個普通函式而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的代碼,所以執行路徑還是只有一條,根本就沒有執行緒的特征,所以在多執行緒執行時要使用start()方法而不是run()方法,
21.為什么執行緒通信的方法 wait(), notify()和 notifyAll()被定義在 Object 類里?
-
因為Java所有類的都繼承了Object,Java想讓任何物件都可以作為鎖,并且 wait(),notify()等方法用于等待物件的鎖或者喚醒執行緒,在 Java 的執行緒中并沒有可供任何物件使用的鎖,所以任意物件呼叫方法一定定義在Object類中,
-
有的人會說,既然是執行緒放棄物件鎖,那也可以把wait()定義在Thread類里面啊,新定義的執行緒繼承于Thread類,也不需要重新定義wait()方法的實作,然而,這樣做有一個非常大的問題,一個執行緒完全可以持有很多鎖,你一個執行緒放棄鎖的時候,到底要放棄哪個鎖?當然了,這種設計并不是不能實作,只是管理起來更加復雜,
22.為什么 wait(), notify()和 notifyAll()必須在同步方法或者同步塊中被呼叫?
- 當一個執行緒需要呼叫物件的 wait()方法的時候,這個執行緒必須擁有該物件的鎖,接著它就會釋放這個物件鎖并進入等待狀態直到其他執行緒呼叫這個物件上的 notify()方法,同樣的,當一個執行緒需要呼叫物件的 notify()方法時,它會釋放這個物件的鎖,以便其他在等待的執行緒就可以得到這個物件鎖,由于所有的這些方法都需要執行緒持有物件的鎖,這樣就只能通過同步來實作,所以他們只能在同步方法或者同步塊中被呼叫,
23.創建多少個執行緒合適?
創建多少執行緒合適,要看多執行緒具體的應用場景,我們的程式一般都是 CPU 計算和 I/O 操作交叉執行的,由于 I/O 設備的速度相對于 CPU 來說都很慢,所以大部分情況下,I/O 操作執行的時間相對于 CPU 計算來說都非常長,這種場景我們一般都稱為 I/O 密集型計算;和 I/O 密集型計算相對的就是 CPU 密集型計算了,CPU 密集型計算大部分場景下都是純 CPU 計算,I/O 密集型程式和 CPU 密集型程式,計算最佳執行緒數的方法是不同的,
下面我們對這兩個場景分別說明,
①對于 CPU 密集型計算,多執行緒本質上是提升多核 CPU 的利用率,所以對于一個 4 核的 CPU,每個核一個執行緒,理論上創建 4 個執行緒就可以了,再多創建執行緒也只是增加執行緒切換的成本,所以,對于 CPU 密集型的計算場景,理論上“執行緒的數量 =CPU 核數”就是最合適的,不過在工程上,執行緒的數量一般會設定為“CPU 核數 +1”,這樣的話,當執行緒因為偶爾的記憶體頁失效或其他原因導致阻塞時,這個額外的執行緒可以頂上,從而保證 CPU 的利用率,
②對于 I/O 密集型的計算場景,比如前面我們的例子中,如果 CPU 計算和 I/O 操作的耗時是 1:1,那么 2 個執行緒是最合適的,如果 CPU 計算和 I/O 操作的耗時是 1:2,那多少個執行緒合適呢?是 3 個執行緒,如下圖所示:CPU 在 A、B、C 三個執行緒之間切換,對于執行緒 A,當 CPU 從 B、C 切換回來時,執行緒 A 正好執行完 I/O 操作,這樣 CPU 和 I/O 設備的利用率都達到了 100%,
對于 I/O 密集型計算場景,最佳的執行緒數是與程式中 CPU 計算和 I/O 操作的耗時比相關的,我們可以總結出這樣一個公式:
最佳執行緒數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]
24.什么是執行緒池?
- Java中的執行緒池是運用場景最多的并發框架,幾乎所有需要異步或并發執行任務的程式都可以使用執行緒池,在開發程序中,合理地使用執行緒池能夠帶來許多好處,
- 降低資源消耗,通過重復利用已創建的執行緒降低執行緒創建和銷毀造成的消耗,
- 提高回應速度,當任務到達時,任務可以不需要等到執行緒創建就能立即執行,
- 提高執行緒的可管理性,執行緒是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控,但是,要做到合理利用
25.創建執行緒池有哪幾種方式?
(1)newFixedThreadPool(int nThreads)
創建一個固定長度的執行緒池,每當提交一個任務就創建一個執行緒,直到達到執行緒池的最大數量,這時執行緒規模將不再變化,當執行緒發生未預期的錯誤而結束時,執行緒池會補充一個新的執行緒,
(2)newCachedThreadPool()
創建一個可快取的執行緒池,如果執行緒池的規模超過了處理需求,將自動回收空閑執行緒,而當需求增加時,則可以自動添加新執行緒,執行緒池的規模不存在任何限制,
(3)newSingleThreadExecutor()
這是一個單執行緒的Executor,它創建單個作業執行緒來執行任務,如果這個執行緒例外結束,會創建一個新的來替代它;它的特點是能確保依照任務在佇列中的順序來串行執行,
(4)newScheduledThreadPool(int corePoolSize)
創建了一個固定長度的執行緒池,而且以延遲或定時的方式來執行任務,類似于Timer,
26.執行緒池作業原理
當向執行緒池提交一個任務之后,執行緒池是如何處理這個任務的呢?處理流程圖如圖所示,

當提交一個新任務到執行緒池時,執行緒池的處理流程如下:
1)執行緒池判斷核心執行緒池里的執行緒是否都在執行任務,如果不是,則創建一個新的作業執行緒來執行任務,如果核心執行緒池里的執行緒都在執行任務,則進入下個流程,
2)執行緒池判斷作業佇列是否已經滿,如果作業佇列沒有滿,則將新提交的任務存盤在這個作業佇列里,如果作業佇列滿了,則進入下個流程,
3)執行緒池判斷執行緒池的執行緒是否都處于作業狀態,如果沒有,則創建一個新的作業執行緒來執行任務,如果已經滿了,則交給飽和策略來處理這個任務,
27.執行緒池有什么作用?
-
執行緒池是為突然大量爆發的執行緒設計的,通過有限的幾個固定執行緒為大量的操作服務,減少了創建和銷毀執行緒所需的時間,從而提高效率,
-
如果一個執行緒所需要執行的時間非常長的話,就沒必要用執行緒池了(不是不能作長時間操作,而是不宜,本來降低執行緒創建和銷毀,結果你那么久我還不好控制還不如直接創建執行緒),況且我們還不能控制執行緒池中執行緒的開始、掛起、和中止,
28.執行緒池有什么優點?
-
降低資源消耗:重用存在的執行緒,減少物件創建銷毀的開銷,
-
提高回應速度,可有效的控制最大并發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞,當任務到達時,任務可以不需要的等到執行緒創建就能立即執行,
-
提高執行緒的可管理性,執行緒是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控,
-
附加功能:提供定時執行、定期執行、單執行緒、并發數控制等功能,
29.什么是阻塞式方法?
- 阻塞式方法是指程式會一直等待該方法完成期間不做其他事情,ServerSocket 的accept()方法就是一直等待客戶端連接,這里的阻塞是指呼叫結果回傳之前,當前執行緒會被掛起,直到得到結果之后才會回傳,此外,還有異步和非阻塞式方法在任務完成前就回傳,
30.Java 中你怎樣喚醒一個阻塞的執行緒?
-
首先 ,wait()、notify() 方法是針對物件的,呼叫任意物件的 wait()方法都將導致執行緒阻塞,阻塞的同時也將釋放該物件的鎖,相應地,呼叫任意物件的 notify()方法則將隨機解除該物件阻塞的執行緒,但它需要重新獲取該物件的鎖,直到獲取成功才能往下執行;
-
其次,wait、notify 方法必須在 synchronized 塊或方法中被呼叫,并且要保證同步塊或方法的鎖物件與呼叫 wait、notify 方法的物件是同一個,如此一來在呼叫 wait 之前當前執行緒就已經成功獲取某物件的鎖,執行 wait 阻塞后當前執行緒就將之前獲取的物件鎖釋放,
31.如何在兩個執行緒間共享資料?
- 在兩個執行緒間共享變數即可實作共享,
一般來說,共享變數要求變數本身是執行緒安全的,然后在執行緒內使用的時候,如果有對共享變數的復合操作,那么也得保證復合操作的執行緒安全性,
32.Java 如何實作多執行緒之間的通訊和協作?
-
可以通過中斷 和 共享變數的方式實作執行緒間的通訊和協作
-
比如說最經典的生產者-消費者模型:當佇列滿時,生產者需要等待佇列有空間才能繼續往里面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即佇列)的占用權,因為生產者如果不釋放對臨界資源的占用權,那么消費者就無法消費佇列中的商品,就不會讓佇列有空間,那么生產者就會一直無限等待下去,因此,一般情況下,當佇列滿時,會讓生產者交出對臨界資源的占用權,并進入掛起狀態,然后等待消費者消費了商品,然后消費者通知生產者佇列有空間了,同樣地,當佇列空時,消費者也必須等待,等待生產者通知它佇列中有商品了,這種互相通信的程序就是執行緒間的協作,
-
Java中執行緒通信協作的最常見方式:
-
一.syncrhoized加鎖的執行緒的Object類的wait()/notify()/notifyAll()
-
二.ReentrantLock類加鎖的執行緒的Condition類的await()/signal()/signalAll()
-
-
執行緒間直接的資料交換:
- 三.通過管道進行執行緒間通信:位元組流、字符流
33.同步方法和同步塊,哪個是更好的選擇?
-
同步塊是更好的選擇,因為它不會鎖住整個物件(當然你也可以讓它鎖住整個物件),同步方法會鎖住整個物件,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行并需要等待獲得這個物件上的鎖,
-
同步塊更要符合開放呼叫的原則,只在需要鎖住的代碼塊鎖住相應的物件,這樣從側面來說也可以避免死鎖,
34.什么是執行緒同步和執行緒互斥,有哪幾種實作方式?
-
當一個執行緒對共享的資料進行操作時,應使之成為一個”原子操作“,即在沒有完成相關操作之前,不允許其他執行緒打斷它,否則,就會破壞資料的完整性,必然會得到錯誤的處理結果,這就是執行緒的同步,
-
在多執行緒應用中,考慮不同執行緒之間的資料同步和防止死鎖,當兩個或多個執行緒之間同時等待對方釋放資源的時候就會形成執行緒之間的死鎖,為了防止死鎖的發生,需要通過同步來實作執行緒安全,
-
執行緒互斥是指對于共享的行程系統資源,在各單個執行緒訪問時的排它性,當有若干個執行緒都要使用某一共享資源時,任何時刻最多只允許一個執行緒去使用,其它要使用該資源的執行緒必須等待,直到占用資源者釋放該資源,執行緒互斥可以看成是一種特殊的執行緒同步,
-
執行緒間的同步方法大體可分為兩類:用戶模式和內核模式,顧名思義,內核模式就是指利用系統內核物件的單一性來進行同步,使用時需要切換內核態與用戶態,而用戶模式就是不需要切換到內核態,只在用戶態完成操作,
-
用戶模式下的方法有:原子操作(例如一個單一的全域變數),臨界區,內核模式下的方法有:事件,信號量,互斥量,
-
實作執行緒同步的方法
-
同步代碼方法:sychronized 關鍵字修飾的方法
-
同步代碼塊:sychronized 關鍵字修飾的代碼塊
-
使用特殊變數域volatile實作執行緒同步:volatile關鍵字為域變數的訪問提供了一種免鎖機制
-
使用重入鎖實作執行緒同步:reentrantlock類是可沖入、互斥、實作了lock介面的鎖他與sychronized方法具有相同的基本行為和語意
-
35.在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?
-
在 java 虛擬機中,監視器和鎖在Java虛擬機中是一塊使用的,監視器監視一塊同步代碼塊,確保一次只有一個執行緒執行同步代碼塊,每一個監視器都和一個物件參考相關聯,執行緒在獲取鎖之前不允許執行同步代碼,
-
一旦方法或者代碼塊被 synchronized 修飾,那么這個部分就放入了監視器的監視區域,確保一次只能有一個執行緒執行該部分的代碼,執行緒在獲取鎖之前不允許執行該部分的代碼
-
另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案
36.如果你提交任務時,執行緒池佇列已滿,這時會發生什么
-
有倆種可能:
(1)如果使用的是無界佇列 LinkedBlockingQueue,也就是無界佇列的話,沒關系,繼續添加任務到阻塞佇列中等待執行,因為 LinkedBlockingQueue 可以近乎認為是一個無窮大的佇列,可以無限存放任務
(2)如果使用的是有界佇列比如 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增加執行緒數量,如果增加了執行緒數量還是處理不過來,ArrayBlockingQueue 繼續滿,那么則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy
37.什么叫執行緒安全?servlet 是執行緒安全嗎?
-
執行緒安全是編程中的術語,指某個方法在多執行緒環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變數,使程式功能正確完成,
-
Servlet 不是執行緒安全的,servlet 是單實體多執行緒的,當多個執行緒同時訪問同一個方法,是不能保證共享變數的執行緒安全性的,
-
Struts2 的 action 是多實體多執行緒的,是執行緒安全的,每個請求過來都會 new 一個新的 action 分配給這個請求,請求完成后銷毀,
-
SpringMVC 的 Controller 是執行緒安全的嗎?不是的,和 Servlet 類似的處理流程,
-
Struts2 好處是不用考慮執行緒安全問題;Servlet 和 SpringMVC 需要考慮執行緒安全問題,但是性能可以提升不用處理太多的 gc,可以使用 ThreadLocal 來處理多執行緒的問題,
38.在 Java 程式中怎么保證多執行緒的運行安全?
-
方法一:使用安全類,比如 java.util.concurrent 下的類,使用原子類AtomicInteger
-
方法二:使用自動鎖 synchronized,
-
方法三:使用手動鎖 Lock,
-
手動鎖 Java 示例代碼如下:
Lock lock = new ReentrantLock(); lock. lock(); try { System. out. println("獲得鎖"); } catch (Exception e) { } finally { System. out. println("釋放鎖"); lock. unlock(); }
39.你對執行緒優先級的理解是什么?
-
每一個執行緒都是有優先級的,一般來說,高優先級的執行緒在運行時會具有優先權,但這依賴于執行緒調度的實作,這個實作是和作業系統相關的(OS dependent),我們可以定義執行緒的優先級,但是這并不能保證高優先級的執行緒會在低優先級的執行緒前執行,執行緒優先級是一個 int 變數(從 1-10),1 代表最低優先級,10 代表最高優先級,
-
Java 的執行緒優先級調度會委托給作業系統去處理,所以與具體的作業系統優先級有關,如非特別需要,一般無需設定執行緒優先級,
-
當然,如果你真的想設定優先級可以通過setPriority()方法設定,但是設定了不一定會該變,這個是不準確的
40.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
-
這是一個非常刁鉆和狡猾的問題,請記住:執行緒類的構造方法、靜態塊是被 new這個執行緒類所在的執行緒所呼叫的,而 run 方法里面的代碼才是被執行緒自身所呼叫的,
-
如果說上面的說法讓你感到困惑,那么我舉個例子,假設 Thread2 中 new 了Thread1,main 函式中 new 了 Thread2,那么:
(1)Thread2 的構造方法、靜態塊是 main 執行緒呼叫的,Thread2 的 run()方法是Thread2 自己呼叫的
(2)Thread1 的構造方法、靜態塊是 Thread2 呼叫的,Thread1 的 run()方法是Thread1 自己呼叫的
41.Java 中怎么獲取一份執行緒 dump 檔案?你如何在 Java 中獲取執行緒堆疊?
-
Dump檔案是行程的記憶體鏡像,可以把程式的執行狀態通過除錯器保存到dump檔案中,
-
在 Linux 下,你可以通過命令 kill -3 PID (Java 行程的行程 ID)來獲取 Java應用的 dump 檔案,
-
在 Windows 下,你可以按下 Ctrl + Break 來獲取,這樣 JVM 就會將執行緒的 dump 檔案列印到標準輸出或錯誤檔案中,它可能列印在控制臺或者日志檔案中,具體位置依賴應用的配置,
42.一個執行緒運行時發生例外會怎樣?
- 如果例外沒有被捕獲該執行緒將會停止執行,Thread.UncaughtExceptionHandler是用于處理未捕獲例外造成執行緒突然中斷情況的一個內嵌介面,當一個未捕獲例外將造成執行緒中斷的時候,JVM 會使用 Thread.getUncaughtExceptionHandler()來查詢執行緒的 UncaughtExceptionHandler 并將執行緒和例外作為引數傳遞給 handler 的 uncaughtException()方法進行處理,
43.Java 執行緒數過多會造成什么例外?
-
執行緒的生命周期開銷非常高
-
消耗過多的 CPU
資源如果可運行的執行緒數量多于可用處理器的數量,那么有執行緒將會被閑置,大量空閑的執行緒會占用許多記憶體,給垃圾回收器帶來壓力,而且大量的執行緒在競爭 CPU資源時還將產生其他性能的開銷,
-
降低穩定性JVM
在可創建執行緒的數量上存在一個限制,這個限制值將隨著平臺的不同而不同,并且承受著多個因素制約,包括 JVM 的啟動引數、Thread 建構式中請求堆疊的大小,以及底層作業系統對執行緒的限制等,如果破壞了這些限制,那么可能拋出OutOfMemoryError 例外,
44.多執行緒的常用方法
| 方法 名 | 描述 |
|---|---|
| sleep() | 強迫一個執行緒睡眠N毫秒 |
| isAlive() | 判斷一個執行緒是否存活, |
| join() | 等待執行緒終止, |
| activeCount() | 程式中活躍的執行緒數, |
| enumerate() | 列舉程式中的執行緒, |
| currentThread() | 得到當前執行緒, |
| isDaemon() | 一個執行緒是否為守護執行緒, |
| setDaemon() | 設定一個執行緒為守護執行緒, |
| setName() | 為執行緒設定一個名稱, |
| wait() | 強迫一個執行緒等待, |
| notify() | 通知一個執行緒繼續運行, |
| setPriority() | 設定一個執行緒的優先級, |
45.Java中垃圾回收有什么目的?什么時候進行垃圾回收?
-
垃圾回收是在記憶體中存在沒有參考的物件或超過作用域的物件時進行的,
-
垃圾回收的目的是識別并且丟棄應用不再使用的物件來釋放和重用資源,
46.執行緒之間如何通信及執行緒之間如何同步?
-
在并發編程中,我們需要處理兩個關鍵問題:執行緒之間如何通信及執行緒之間如何同步,通信是指執行緒之間以如何來交換資訊,一般執行緒之間的通信機制有兩種:共享記憶體和訊息傳遞,
-
Java的并發采用的是共享記憶體模型,Java執行緒之間的通信總是隱式進行,整個通信程序對程式員完全透明,如果撰寫多執行緒程式的Java程式員不理解隱式進行的執行緒之間通信的作業機制,很可能會遇到各種奇怪的記憶體可見性問題,
47.Java記憶體模型
-
共享記憶體模型指的就是Java記憶體模型(簡稱JMM),JMM決定一個執行緒對共享變數的寫入時,能對另一個執行緒可見,從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關系:執行緒之間的共享變數存盤在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中存盤了該執行緒以讀/寫共享變數的副本,本地記憶體是JMM的一個抽象概念,并不真實存在,它涵蓋了快取,寫緩沖區,暫存器以及其他的硬體和編譯器優化,

-
從上圖來看,執行緒A與執行緒B之間如要通信的話,必須要經歷下面2個步驟:
- 首先,執行緒A把本地記憶體A中更新過的共享變數重繪到主記憶體中去,
- 然后,執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數,
下面通過示意圖來說明執行緒之間的通信

- 總結:什么是Java記憶體模型:java記憶體模型簡稱jmm,定義了一個執行緒對另一個執行緒可見,共享變數存放在主記憶體中,每個執行緒都有自己的本地記憶體,當多個執行緒同時訪問一個資料的時候,可能本地記憶體沒有及時重繪到主記憶體,所以就會發生執行緒安全問題,
48.如果物件的參考被置為null,垃圾收集器是否會立即釋放物件占用的記憶體?
-
不會,在下一個垃圾回呼周期中,這個物件將是被可回收的,
-
也就是說并不會立即被垃圾收集器立刻回收,而是在下一次垃圾回收時才會釋放其占用的記憶體,
49.finalize()方法什么時候被呼叫?解構式(finalization)的目的是什么?
-
1.垃圾回收器(garbage colector)決定回收某物件時,就會運行該物件的finalize()方法;
finalize是Object類的一個方法,該方法在Object類中的宣告protected void finalize() throws Throwable { }
在垃圾回收器執行時會呼叫被回收物件的finalize()方法,可以覆寫此方法來實作對其資源的回收,注意:一旦垃圾回收器準備釋放物件占用的記憶體,將首先呼叫該物件的finalize()方法,并且下一次垃圾回收動作發生時,才真正回收物件占用的記憶體空間 -
2.GC本來就是記憶體回收了,應用還需要在finalization做什么呢? 答案是大部分時候,什么都不用做(也就是不需要多載),只有在某些很特殊的情況下,比如你呼叫了一些native的方法(一般是C寫的),可以要在finaliztion里去呼叫C的釋放函式,
- Finalizetion主要用來釋放被物件占用的資源(不是指記憶體,而是指其他資源,比如檔案(File Handle)、埠(ports)、資料庫連接(DB Connection)等),然而,它不能真正有效地作業,
50.什么是重排序?
- 程式執行的順序按照代碼的先后順序執行,
- 一般來說處理器為了提高程式運行效率,可能會對輸入代碼進行優化,進行重新排序(重排序),它不保證程式中各個陳述句的執行先后順序同代碼中的順序一致,但是它會保證程式最終執行結果和代碼順序執行的結果是一致的,
int a = 5;
int r = 3;
a = a + 2;
r = a*a;
- 則因為重排序,他還可能執行順序為(這里標注的是陳述句的執行順序) 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因為這打破了依賴關系, - 顯然重排序對單執行緒運行是不會有任何問題,但是多執行緒就不一定了,所以我們在多執行緒編程時就得考慮這個問題了,
51.重排序實際執行的指令步驟

- 編譯器優化的重排序,編譯器在不改變單執行緒程式語意的前提下,可以重新安排陳述句的執行順序,
- 指令級并行的重排序,現代處理器采用了指令級并行技術(ILP)來將多條指令重疊執行,如果不存在資料依賴性,處理器可以改變陳述句對應機器指令的執行順序,
- 記憶體系統的重排序,由于處理器使用快取和讀/寫緩沖區,這使得加載和存盤操作看上去可能是在亂序執行,
- 這些重排序對于單執行緒沒問題,但是多執行緒都可能會導致多執行緒程式出現記憶體可見性問題,
52.重排序遵守的規則
- as-if-serial:
- 不管怎么排序,結果不能改變
- 不存在資料依賴的可以被編譯器和處理器重排序
- 一個操作依賴兩個操作,這兩個操作如果不存在依賴可以重排序
- 單執行緒根據此規則不會有問題,但是重排序后多執行緒會有問題
53.as-if-serial規則和happens-before規則的區別
-
as-if-serial語意保證單執行緒內程式的執行結果不被改變,happens-before關系保證正確同步的多執行緒程式的執行結果不被改變,
-
as-if-serial語意給撰寫單執行緒程式的程式員創造了一個幻境:單執行緒程式是按程式的順序來執行的,happens-before關系給撰寫正確同步的多執行緒程式的程式員創造了一個幻境:正確同步的多執行緒程式是按happens-before指定的順序來執行的,
-
as-if-serial語意和happens-before這么做的目的,都是為了在不改變程式執行結果的前提下,盡可能地提高程式執行的并行度,
54.并發關鍵字 synchronized
-
在 Java 中,synchronized 關鍵字是用來控制執行緒同步的,就是在多執行緒的環境下,控制 synchronized 代碼段不被多個執行緒同時執行,synchronized 可以修飾類、方法、變數,
-
另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的作業系統的 Mutex Lock 來實作的,Java 的執行緒是映射到作業系統的原生執行緒之上的,如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實作執行緒之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的 synchronized 效率低的原因,慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了,JDK1.6對鎖的實作引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷,
55.說說自己是怎么使用 synchronized 關鍵字,在專案中用到了嗎
synchronized關鍵字最主要的三種使用方式:
- 修飾實體方法: 作用于當前物件實體加鎖,進入同步代碼前要獲得當前物件實體的鎖
- 修飾靜態方法: 也就是給當前類加鎖,會作用于類的所有物件實體,因為靜態成員不屬于任何一個實體物件,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個物件,只有一份),所以如果一個執行緒A呼叫一個實體物件的非靜態 synchronized 方法,而執行緒B需要呼叫這個實體物件所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法占用的鎖是當前實體物件鎖,
- 修飾代碼塊: 指定加鎖物件,對給定物件加鎖,進入同步代碼庫前要獲得給定物件的鎖,
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖,synchronized 關鍵字加到實體方法上是給物件實體上鎖,盡量不要使用 synchronized(String a) 因為JVM中,字串常量池具有快取功能!
56.單例模式了解嗎?給我解釋一下雙重檢驗鎖方式實作單例模式
雙重校驗鎖實作物件單例(執行緒安全)
說明:
-
雙鎖機制的出現是為了解決前面同步問題和性能問題,看下面的代碼,簡單分析下確實是解決了多執行緒并行進來不會出現重復new物件,而且也實作了懶加載
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() {} public static Singleton getUniqueInstance() { //先判斷物件是否已經實體過,沒有實體化過才進入加鎖代碼 if (uniqueInstance == null) { //類物件加鎖 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}
另外,需要注意 uniqueInstance 采用 volatile 關鍵字修飾也是很有必要,
uniqueInstance 采用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行:
- 為 uniqueInstance 分配記憶體空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的記憶體地址
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2,指令重排在單執行緒環境下不會出現問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的實體,例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 后發現 uniqueInstance 不為空,因此回傳 uniqueInstance,但此時 uniqueInstance 還未被初始化,
使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常運行,
57.說一下 synchronized 底層實作原理?
-
Synchronized的語意底層是通過一個monitor(監視器鎖)的物件來完成,
-
每個物件有一個監視器鎖(monitor),每個Synchronized修飾過的代碼當它的monitor被占用時就會處于鎖定狀態并且嘗試獲取monitor的所有權 ,程序:
1、如果monitor的進入數為0,則該執行緒進入monitor,然后將進入數設定為1,該執行緒即為monitor的所有者,
2、如果執行緒已經占有該monitor,只是重新進入,則進入monitor的進入數加1.
3、如果其他執行緒已經占用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權,
synchronized是可以通過 反匯編指令 javap命令,查看相應的位元組碼檔案,
58.synchronized可重入的原理
- 重入鎖是指一個執行緒獲取到該鎖之后,該執行緒可以繼續獲得該鎖,底層原理維護一個計數器,當執行緒獲取該鎖時,計數器加一,再次獲得該鎖時繼續加一,釋放鎖時,計數器減一,當計數器值為0時,表明該鎖未被任何執行緒所持有,其它執行緒可以競爭獲取鎖,
59.什么是自旋
通過自旋鎖,可以減少執行緒阻塞造成的執行緒切換(包括掛起執行緒和恢復執行緒),頻繁的阻塞和喚醒對CPU來說是一件負擔很重的作業,勢必會給系統的并發性能帶來很大的壓力,同時我們發現在許多應用上面,物件鎖的鎖狀態只會持續很短一段時間,為了這一段很短的時間頻繁地阻塞和喚醒執行緒是非常不值得的,所以引入自旋鎖,
何謂自旋鎖? 所謂自旋鎖,就是讓該執行緒等待一段時間,不會被立即掛起,看持有鎖的執行緒是否會很快釋放鎖,怎么等待呢?執行一段無意義的回圈即可(自旋), 自旋等待不能替代阻塞,先不說對處理器數量的要求(多核,貌似現在沒有單核的處理器了),雖然它可以避免執行緒切換帶來的開銷,但是它占用了處理器的時間,如果持有鎖的執行緒很快就釋放了鎖,那么自旋的效率就非常好,反之,自旋的執行緒就會白白消耗掉處理的資源,它不會做任何有意義的作業,典型的占著茅坑不拉屎,這樣反而會帶來性能上的浪費,所以說,自旋等待的時間(自旋的次數)必須要有一個限度,如果自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起, 自旋鎖在JDK 1.4.2中引入,默認關閉,但是可以使用-XX:+UseSpinning開開啟,在JDK1.6中默認開啟,同時自旋的默認次數為10次,可以通過引數-XX:PreBlockSpin來調整; 如果通過引數-XX:preBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便,假如我將引數調整為10,但是系統很多執行緒都是等你剛剛退出的時候就釋放了鎖(假如你多自旋一兩次就可以獲取鎖),你是不是很尷尬,于是JDK1.6引入自適應的自旋鎖,讓虛擬機會變得越來越聰明,
如果鎖競爭的時間比較長,那么自旋通常不能獲得鎖,白白浪費了自旋占用的CPU時間,這通常發生在鎖持有時間長,且競爭激烈的場景中,此時應主動禁用自旋鎖,
60.多執行緒中 synchronized 鎖升級的原理是什么?
- synchronized 鎖升級原理:在鎖物件的物件頭里面有一個 threadid 欄位,在第一次訪問的時候 threadid 為空,jvm 讓其持有偏向鎖,并將 threadid 設定為其執行緒 id,再次進入的時候會先判斷 threadid 是否與其執行緒 id 一致,如果一致則可以直接使用此物件,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋回圈一定次數來獲取鎖,執行一定次數之后,如果還沒有正常獲取到要使用的物件,此時就會把鎖從輕量級升級為重量級鎖,此程序就構成了 synchronized 鎖的升級,
鎖的升級的目的:鎖升級是為了減低了鎖帶來的性能消耗,在 Java 6 之后優化 synchronized 的實作方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗,
-
偏向鎖,顧名思義,它會偏向于第一個訪問鎖的執行緒,如果在運行程序中,同步鎖只有一個執行緒訪問,不存在多執行緒爭用的情況,則執行緒是不需要觸發同步的,減少加鎖/解鎖的一些CAS操作(比如等待佇列的一些CAS操作),這種情況下,就會給執行緒加一個偏向鎖, 如果在運行程序中,遇到了其他執行緒搶占鎖,則持有偏向鎖的執行緒會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖,
-
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候,輕量級鎖就會升級為重量級鎖;
-
重量級鎖是synchronized ,是 Java 虛擬機中最為基礎的鎖實作,在這種狀態下,Java 虛擬機會阻塞加鎖失敗的執行緒,并且在目標鎖被釋放的時候,喚醒這些執行緒,
61.執行緒 B 怎么知道執行緒 A 修改了變數
-
(1)volatile 修飾變數
-
(2)synchronized 修飾修改變數的方法
-
(3)wait/notify
-
(4)while 輪詢
62.當一個執行緒進入一個物件的 synchronized 方法 A 之后,其它執行緒是否可進入此物件的 synchronized 方法 B?
- 不能,其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入,因為非靜態方法上的 synchronized 修飾符要求執行方法時要獲得物件的鎖,如果已經進入A 方法說明物件鎖已經被取走,那么試圖進入 B 方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖,
63.synchronized、volatile、CAS 比較
-
(1)synchronized 是悲觀鎖,屬于搶占式,會引起其他執行緒阻塞,
-
(2)volatile 提供多執行緒共享變數可見性和禁止指令重排序優化,
-
(3)CAS 是基于沖突檢測的樂觀鎖(非阻塞)
64.synchronized 和 Lock 有什么區別?
- 首先synchronized是Java內置關鍵字,在JVM層面,Lock是個Java類;
- synchronized 可以給類、方法、代碼塊加鎖;而 lock 只能給代碼塊加鎖,
- synchronized 不需要手動獲取鎖和釋放鎖,使用簡單,發生例外會自動釋放鎖,不會造成死鎖;而 lock 需要自己加鎖和釋放鎖,如果使用不當沒有 unLock()去釋放鎖就會造成死鎖,
- 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到,
65.synchronized 和 ReentrantLock 區別是什么?
-
synchronized 是和 if、else、for、while 一樣的關鍵字,ReentrantLock 是類,這是二者的本質區別,既然 ReentrantLock 是類,那么它就提供了比synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數
-
synchronized 早期的實作比較低效,對比 ReentrantLock,大多數場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進,
-
相同點:兩者都是可重入鎖
兩者都是可重入鎖,“可重入鎖”概念是:自己可以再次獲取自己的內部鎖,比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖,同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖,
-
主要區別如下:
- ReentrantLock 使用起來比較靈活,但是必須有釋放鎖的配合動作;
- ReentrantLock 必須手動獲取與釋放鎖,而 synchronized 不需要手動釋放和開啟鎖;
- ReentrantLock 只適用于代碼塊鎖,而 synchronized 可以修飾類、方法、變數等,
- 二者的鎖機制其實也是不一樣的,ReentrantLock 底層呼叫的是 Unsafe 的park 方法加鎖,synchronized 操作的應該是物件頭中 mark word
-
Java中每一個物件都可以作為鎖,這是synchronized實作同步的基礎:
- 普通同步方法,鎖是當前實體物件
- 靜態同步方法,鎖是當前類的class物件
- 同步方法塊,鎖是括號里面的物件
66.volatile 關鍵字的作用
-
對于可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排, volatile 提供 happens-before 的保證,確保一個執行緒的修改能對其他執行緒是可見的,當一個共享變數被 volatile 修飾時,它會保證修改的值會立即被更新到主記憶體中,當有其他執行緒需要讀取時,它會去記憶體中讀取新值,
-
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger,
-
volatile 常用于多執行緒環境下的單次操作(單次讀或者單次寫),
67.Java 中能創建 volatile 陣列嗎?
- 能,Java 中可以創建 volatile 型別陣列,不過只是一個指向陣列的參考,而不是整個陣列,意思是,如果改變參考指向的陣列,將會受到 volatile 的保護,但是如果多個執行緒同時改變陣列的元素,volatile 標示符就不能起到之前的保護作用了,
68.volatile 變數和 atomic 變數有什么不同?
-
volatile 變數可以確保先行關系,即寫操作會發生在后續的讀操作之前, 但它并不能保證原子性,例如用 volatile 修飾 count 變數,那么 count++ 操作就不是原子性的,
-
而 AtomicInteger 類提供的 atomic 方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它資料型別和參考變數也可以進行相似操作,
69.volatile 能使得一個非原子操作變成原子操作嗎?
-
關鍵字volatile的主要作用是使變數在多個執行緒間可見,但無法保證原子性,對于多個執行緒訪問同一個實體變數需要加鎖進行同步,
-
雖然volatile只能保證可見性不能保證原子性,但用volatile修飾long和double可以保證其操作原子性,
所以從Oracle Java Spec里面可以看到:
- 對于64位的long和double,如果沒有被volatile修飾,那么對其操作可以不是原子的,在操作的時候,可以分成兩步,每次對32位操作,
- 如果使用volatile修飾long和double,那么其讀寫都是原子操作
- 對于64位的參考地址的讀寫,都是原子操作
- 在實作JVM時,可以自由選擇是否把讀寫long和double作為原子操作
- 推薦JVM實作為原子操作
70.synchronized 和 volatile 的區別是什么?
-
synchronized 表示只有一個執行緒可以獲取作用物件的鎖,執行代碼,阻塞其他執行緒,
-
volatile 表示變數在 CPU 的暫存器中是不確定的,必須從主存中讀取,保證多執行緒環境下變數的可見性;禁止指令重排序,
區別
-
volatile 是變數修飾符;synchronized 可以修飾類、方法、變數,
-
volatile 僅能實作變數的修改可見性,不能保證原子性;而 synchronized 則可以保證變數的修改可見性和原子性,
-
volatile 不會造成執行緒的阻塞;synchronized 可能會造成執行緒的阻塞,
-
volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化,
-
volatile關鍵字是執行緒同步的輕量級實作,所以volatile性能肯定比synchronized關鍵字要好,但是volatile關鍵字只能用于變數而synchronized關鍵字可以修飾方法以及代碼塊,synchronized關鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之后執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些,
71.final不可變物件,它對寫并發應用有什么幫助?
-
不可變物件(Immutable Objects)即物件一旦被創建它的狀態(物件的資料,也即物件屬性值)就不能改變,反之即為可變物件(Mutable Objects),
-
不可變物件的類即為不可變類(Immutable Class),Java 平臺類別庫中包含許多不可變類,如 String、基本型別的包裝類、BigInteger 和 BigDecimal 等,
-
只有滿足如下狀態,一個物件才是不可變的;
-
它的狀態不能在創建后再被修改;
-
所有域都是 final 型別;并且,它被正確創建(創建期間沒有發生 this 參考的逸出),
-
不可變物件保證了物件的記憶體可見性,對不可變物件的讀取不需要進行額外的同步手段,提升了代碼執行效率,
72.Lock 介面和synchronized 對比同步它有什么優勢?
-
Lock 介面比同步方法和同步塊提供了更具擴展性的鎖操作,他們允許更靈活的結構,可以具有完全不同的性質,并且可以支持多個相關類的條件物件,
-
它的優勢有:
-
(1)可以使鎖更公平
-
(2)可以使執行緒在等待鎖的時候回應中斷
-
(3)可以讓執行緒嘗試獲取鎖,并在無法獲取鎖的時候立即回傳或者等待一段時間
-
(4)可以在不同的范圍,以不同的順序獲取和釋放鎖
-
-
整體上來說 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件佇列的(newCondition 方法)鎖操作,另外 Lock 的實作類基本都支持非公平鎖(默認)和公平鎖,synchronized 只支持非公平鎖,當然,在大部分情況下,非公平鎖是高效的選擇,
73.樂觀鎖和悲觀鎖的理解及如何實作,有哪些實作方式?
-
悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖,傳統的關系型資料庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖,再比如 Java 里面的同步原語 synchronized 關鍵字的實作也是悲觀鎖,
-
樂觀鎖:顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制,樂觀鎖適用于多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似于 write_condition 機制,其實都是提供的樂觀鎖,在 Java中 java.util.concurrent.atomic 包下面的原子變數類就是使用了樂觀鎖的一種實作方式 CAS 實作的,
74.什么是 CAS
-
CAS 是 compare and swap 的縮寫,即我們所說的比較交換,
-
cas 是一種基于鎖的操作,而且是樂觀鎖,在 java 中鎖分為樂觀鎖和悲觀鎖,悲觀鎖是將資源鎖住,等一個之前獲得鎖的執行緒釋放鎖之后,下一個執行緒才可以訪問,而樂觀鎖采取了一種寬泛的態度,通過某種方式不加鎖來處理資源,比如通過給記錄加 version 來獲取資料,性能較悲觀鎖有很大的提高,
-
CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B),如果記憶體地址里面的值和 A 的值是一樣的,那么就將記憶體里面的值更新成 B,CAS是通過無限回圈來獲取資料的,若果在第一輪回圈中,a 執行緒獲取地址里面的值被b 執行緒修改了,那么 a 執行緒需要自旋,到下次回圈才有可能機會執行,
java.util.concurrent.atomic 包下的類大多是使用 CAS 操作來實作的(AtomicInteger,AtomicBoolean,AtomicLong)
75.CAS 的會產生什么問題?
-
1、ABA 問題:
比如說一個執行緒 one 從記憶體位置 V 中取出 A,這時候另一個執行緒 two 也從記憶體中取出 A,并且 two 進行了一些操作變成了 B,然后 two 又將 V 位置的資料變成 A,這時候執行緒 one 進行 CAS 操作發現記憶體中仍然是 A,然后 one 操作成功,盡管執行緒 one 的 CAS 操作成功,但可能存在潛藏的問題,從 Java1.5 開始 JDK 的 atomic包里提供了一個類 AtomicStampedReference 來解決 ABA 問題,
-
2、回圈時間長開銷大:
對于資源競爭嚴重(執行緒沖突嚴重)的情況,CAS 自旋的概率會比較大,從而浪費更多的 CPU 資源,效率低于 synchronized,
-
3、只能保證一個共享變數的原子操作:
當對一個共享變數執行操作時,我們可以使用回圈 CAS 的方式來保證原子操作,但是對多個共享變數操作時,回圈 CAS 就無法保證操作的原子性,這個時候就可以用鎖,
76.什么是原子類
-
java.util.concurrent.atomic包:是原子類的小工具包,支持在單個變數上解除鎖的執行緒安全編程
原子變數類相當于一種泛化的 volatile 變數,能夠支持原子的和有條件的讀-改-寫操作, -
比如:AtomicInteger 表示一個int型別的值,并提供了 get 和 set 方法,這些 Volatile 型別的int變數在讀取和寫入上有著相同的記憶體語意,它還提供了一個原子的 compareAndSet 方法(如果該方法成功執行,那么將實作與讀取/寫入一個 volatile 變數相同的記憶體效果),以及原子的添加、遞增和遞減等方法,AtomicInteger 表面上非常像一個擴展的 Counter 類,但在發生競爭的情況下能提供更高的可伸縮性,因為它直接利用了硬體對并發的支持,
簡單來說就是原子類來實作CAS無鎖模式的演算法
77.原子類的常用類
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
78.說一下 Atomic的原理?
- Atomic包中的類基本的特性就是在多執行緒環境下,當有多個執行緒同時對單個(包括基本型別及參考型別)變數進行操作時,具有排他性,即當多個執行緒同時對該變數的值進行更新時,僅有一個執行緒能成功,而未成功的執行緒可以向自旋鎖一樣,繼續嘗試,一直等到執行成功,
79.死鎖與活鎖的區別,死鎖與饑餓的區別?
-
死鎖:是指兩個或兩個以上的行程(或執行緒)在執行程序中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去,
-
活鎖:任務或者執行者沒有被阻塞,由于某些條件沒有滿足,導致一直重復嘗試,失敗,嘗試,失敗,
-
活鎖和死鎖的區別在于,處于活鎖的物體是在不斷的改變狀態,這就是所謂的“活”, 而處于死鎖的物體表現為等待;活鎖有可能自行解開,死鎖則不能,
-
饑餓:一個或者多個執行緒因為種種原因無法獲得所需要的資源,導致一直無法執行的狀態,
Java 中導致饑餓的原因:
-
1、高優先級執行緒吞噬所有的低優先級執行緒的 CPU 時間,
-
2、執行緒被永久堵塞在一個等待進入同步塊的狀態,因為其他執行緒總是能在它之前持續地對該同步塊進行訪問,
-
3、執行緒在等待一個本身也處于永久等待完成的物件(比如呼叫這個物件的 wait 方法),因為其他執行緒總是被持續地獲得喚醒,
-
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/298624.html
標籤:其他
