Java 併發基礎
1、什麼是執行緒和程式?2、程式和執行緒的區別是什麼?3、請簡要描述執行緒與程式的關係4、建立執行緒有幾種不同的方式?你喜歡哪一種?為什麼?5、概括的解釋下執行緒的幾種可用狀態。6、守護執行緒是什麼?7、並行和併發有什麼區別?8、runnable 和 callable 有什麼區別?9、為什麼要使用多執行緒呢?10、使用多執行緒可能帶來什麼問題?11、什麼是上下文切換?12、為何要使用同步?13、同步方法和同步程式碼塊的區別是什麼?14、在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?15、sleep() 和 wait() 有什麼區別?16、鎖池和等待池的概念17、notify()和 notifyAll()有什麼區別?
1、什麼是執行緒和程式?
程式是程式的一次執行過程,是系統執行程式的基本單位,因此程式是動態的。系統執行一個程式即是一個程式從建立,執行到消亡的過程。
在 Java 中,當我們啟動 main 函式時其實就是啟動了一個 JVM 的程式,而 main 函式所在的執行緒就是這個程式中的一個執行緒,也稱主執行緒。
執行緒與程式相似,但執行緒是一個比程式更小的執行單位。一個程式在其執行的過程中可以產生多個執行緒。與程式不同的是,同程序下的執行緒共享程式的堆
2、程式和執行緒的區別是什麼?
- 程式是執行中的程式,執行緒是程式的內部的一個執行序列;
- 程式是資源分配的單元,執行緒是執行行單元;
- 程式間切換代價大,執行緒間切換代價小;
- 程式擁有資源多,執行緒擁有資源少;
- 地址空間和其它資源:程式間相互獨立,同一程式的各執行緒間共享。某程式內的執行緒在其它程式不可見;
- 通訊:程式間通訊 IPC,執行緒間可以直接讀寫程式資料段(如全域性變數)來進行通訊——需要程式同步和互斥手段的輔助,以保證資料的一致性;
- 在多執行緒 OS 中,程式不是一個可執行的實體;
3、請簡要描述執行緒與程式的關係
從 JVM 角度說程式和執行緒之間的關係
從上圖可以看出:一個程式中可以有多個執行緒,多個執行緒共享程式的堆和方法區 (JDK1.8 之後的元空間)資源,但是每個執行緒有自己的程式計數器、虛擬機器器棧 和 本地方法棧。
知識點擴充套件
為什麼程式計數器、虛擬機器器棧和本地方法棧是執行緒私有的呢?為什麼堆和方法區是執行緒共享的呢?
程式計數器為什麼是私有的?
程式計數器主要有下面兩個作用:
- 位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。
- 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了。
需要注意的是,如果執行的是 native 方法,那麼程式計數器記錄的是 undefined 地址,只有執行的是 Java 程式碼時程式計數器記錄的才是下一條指令的地址。
所以,程式計數器私有主要是為了執行緒切換後能恢復到正確的執行位置。
虛擬機器器棧和本地方法棧為什麼是私有的?
- 虛擬機器器棧:每個 Java 方法在執行的同時會建立一個棧幀用於儲存區域性變量表、運算元棧、常量池引用等資訊。從方法呼叫直至執行完成的過程,就對應著一個棧幀在 Java 虛擬機器器棧中入棧和出棧的過程。
- 本地方法棧:和虛擬機器器棧所發揮的作用非常相似,區別是: 虛擬機器器棧為虛擬機器器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器器使用到的 Native 方法服務。 在 HotSpot 虛擬機器器中和 Java 虛擬機器器棧合二為一。
所以,為了保證執行緒中的區域性變數不被別的執行緒訪問到,虛擬機器器棧和本地方法棧是執行緒私有的。
堆和方法區是所有執行緒共享的資源,其中堆是程式中最大的一塊記憶體,主要用於存放新建立的物件 (所有物件都在這裡分配記憶體),方法區主要用於存放已被載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
4、建立執行緒有幾種不同的方式?你喜歡哪一種?為什麼?
- 繼承 Thread 類(真正意義上的執行緒類),重寫 run 方法,其中 Thread 是 Runnable 介面的實現。
- 實現 Runnable 介面,並重寫裡面的 run 方法。
- 使用 Executor 框架建立執行緒池。Executor 框架是 juc 裡提供的執行緒池的實現。
- 實現 callable 介面,重寫 call 方法,有返回值。
一般情況下使用 Runnable 介面,避免單繼承的侷限,一個類可以繼承多個介面;適合於資源的共享。
5、概括的解釋下執行緒的幾種可用狀態。
- 新建( new ):新建立了一個執行緒物件。
- 可執行( runnable ):執行緒物件建立後,其他執行緒(比如 main 執行緒)呼叫了該物件 的 start ()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲 取 cpu 的使用權 。
- 執行( running ):可執行狀態( runnable )的執行緒獲得了 cpu 時間片( timeslice ) ,執行程式程式碼。
- 阻塞( block ):阻塞狀態是指執行緒因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice ,暫時停止執行。直到執行緒進入可執行( runnable )狀態,才有機會再次獲得 cpu timeslice 轉到執行( running )狀態。阻塞的情況分三種:
- 等待阻塞:執行( running )的執行緒執行 o.wait ()方法, JVM 會把該執行緒放入等待佇列( waitting queue )中。
- 同步阻塞:執行( running )的執行緒在獲取物件的同步鎖時,若該同步鎖 被別的執行緒佔用,則 JVM 會把該執行緒放入鎖池( lock pool )中。
- 其他阻塞: 執行( running )的執行緒執行 Thread . sleep ( long ms )或 t . join ()方法,或者發出了 I / O 請求時, JVM 會把該執行緒置為阻塞狀態。 當 sleep ()狀態超時、 join ()等待執行緒終止或者超時、或者 I / O 處理完畢時,執行緒重新轉入可執行( runnable )狀態。
- 死亡( dead ):執行緒 run ()、 main () 方法執行結束,或者因異常退出了 run ()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。
6、守護執行緒是什麼?
守護執行緒(即daemon thread),是個服務執行緒,準確地來說就是服務其他的執行緒。
7、並行和併發有什麼區別?
- 並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生。
- 並行是在不同實體上的多個事件,併發是在同一實體上的多個事件。
- 在一臺處理器上“同時”處理多個任務指的是併發,在多臺處理器上同時處理多個任務指的是並行。如 hadoop 分散式叢集。
併發程式設計的目標是充分的利用處理器的每一個核,以達到最高的處理效能。
首先給出結論:“並行”概念是“併發”概念的一個子集。我們經常聽說這樣一個關鍵詞“多執行緒併發程式設計”,一個擁有多個執行緒或者程式的併發程式,但如果沒有多核處理器來執行這個程式,那麼就不能以並行方式來執行程式碼。
如果某個系統支援兩個或者多個動作(Action)同時存在,那麼這個系統就是一個併發系統。
如果某個系統支援兩個或者多個動作同時執行,那麼這個系統就是一個並行系統。
推薦閱讀:併發與並行的區別? - Limbo的回答 - 知乎
8、runnable 和 callable 有什麼區別?
- 實現 Callable 介面的任務執行緒能返回執行結果;而實現 Runnable 介面的任務執行緒不能返回結果;
- Callable 介面的 call()方法允許丟擲異常;而 Runnable 介面的 run()方法的異常只能在內部消化,不能繼續上拋;
注意:Callable 介面支援返回執行結果,此時需要呼叫 FutureTask.get()方法實現,此方法會阻塞主執行緒直到獲取‘將來’結果;當不呼叫此方法時,主執行緒不會阻塞!
package interview.threadLearn;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableImpl implements Callable<String> {
private String acceptStr;
public CallableImpl(String acceptStr){
this.acceptStr = acceptStr;
}
@Override
public String call() throws Exception {
// int i = 1/0;
Thread.sleep(3000);
System.out.println("hello : " + this.acceptStr);
return this.acceptStr + " append some chars and return it!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new CallableImpl("my callable test!");
FutureTask<String> task = new FutureTask<>(callable);
long startTime = System.currentTimeMillis();
//建立執行緒
new Thread(task).start();
// 呼叫get()阻塞主執行緒,反之,執行緒不會阻塞
String result = task.get();
long endTime = System.currentTimeMillis();
System.out.println("hello : " + result);
System.out.println("cast : " + (endTime - startTime) / 1000 + " second!");
}
}
//執行結果為:
hello : my callable test!
hello : my callable test! append some chars and return it!
cast : 3 second!
//如果註釋get()方法,結果變為:
cast : 0 second!
hello : my callable test!
複製程式碼
9、為什麼要使用多執行緒呢?
先從總體上來說:
- 從計算機底層來說:執行緒可以比作是輕量級的程式,是程式執行的最小單位,執行緒間的切換和排程的成本遠遠小於程式。另外,多核 CPU 時代意味著多個執行緒可以同時執行,這減少了執行緒上下文切換的開銷。
- 從當代網際網路發展趨勢來說:現在的系統動不動就要求百萬級甚至千萬級的併發量,而多執行緒併發程式設計正是開發高併發系統的基礎,利用好多執行緒機制可以大大提高系統整體的併發能力以及效能。
再深入到計算機底層來探討:
- 單核時代: 在單核時代多執行緒主要是為了提高 CPU 和 IO 裝置的綜合利用率。舉個例子:當只有一個執行緒的時候會導致 CPU 計算時,IO 裝置空閒;進行 IO 操作時,CPU 空閒。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個執行緒的時候就不一樣了,當一個執行緒執行 CPU 計算時,另外一個執行緒可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
- 多核時代: 多核時代多執行緒主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個執行緒的話,CPU 只會一個 CPU 核心被利用到,而建立多個執行緒就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。
10、使用多執行緒可能帶來什麼問題?
併發程式設計的目的就是為了能提高程式的執行效率,進而提高程式執行速度,但是併發程式設計並不總是能提高程式執行速度的,而且併發程式設計可能會遇到很多問題,比如:記憶體洩漏、上下文切換、死鎖還有受限於硬體和軟體的資源閒置問題。
11、什麼是上下文切換?
多執行緒程式設計中一般執行緒的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU 採取的策略是為每個執行緒分配時間片並輪轉的形式。當一個執行緒的時間片用完的時候就會重新置為就緒狀態,把 CPU 使用權讓給其他執行緒使用,這個過程就屬於一次上下文切換。
概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先儲存自己的狀態,以便下次再切換回到這個任務時,可以再載入這個任務的狀態。任務從儲存到再載入的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作。
Linux 相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
12、為何要使用同步?
Java 允許多執行緒併發控制,當多個執行緒同時操作一個可共享的資源變數時(如資料的增刪改查),將會導致資料不準確,相互之間產生衝突,因此加入同步鎖以避免在該執行緒沒有完成操作之前,被其他執行緒的呼叫, 從而保證了該變數的唯一性和準確性。
13、同步方法和同步程式碼塊的區別是什麼?
同步方法
即有 synchronized 關鍵字修飾的方法。由於 Java 的每個物件都有一個內建鎖,當用此關鍵字修飾方法時, 內建鎖會保護整個方法。在呼叫該方法前,需要獲得內建鎖,否則就處於阻塞狀態。
程式碼如: public synchronized void save(){}
注意: synchronized 關鍵字也可以修飾靜態方法,此時如果呼叫該靜態方法,將會鎖住整個類
同步程式碼塊
即有 synchronized 關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內建鎖,從而實現同步。
程式碼如: synchronized(object){}
注意:同步是一種高開銷的操作,因此應該儘量減少同步的內容。
通常沒有必要同步整個方法,使用 synchronized 程式碼塊同步關鍵程式碼即可。
同步方法預設用 this 或者當前類 class 物件作為鎖。
同步程式碼塊可以選擇以什麼來加鎖,比同步方法要更顆粒化,我們可以選擇只同步會發生問題的部分程式碼而不是整個方法。
14、在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?
在 Java 虛擬機器器中,每個物件( Object 和 class )通過某種邏輯關聯監視器,每個監視器和一個物件引用相關聯,為了實現監視器的互斥功能,每個物件都關聯著一把鎖。
一旦方法或者程式碼塊被 synchronized 修飾,那麼這部分就放入了監視器的監視區域,確保一次只能有一個執行緒執行該部分的程式碼,執行緒在獲取鎖之前不允許執行該部分的程式碼。
另外 Java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案。
15、sleep() 和 wait() 有什麼區別?
- 這兩個方法來自不同的類分別是 Thread 和 Object;
- sleep 方法沒有釋放同步鎖,但是 wait 方法釋放了鎖,使得其他執行緒可以使用同步控制塊;
- sleep 可以在任何地方使用,wait notify notifyall 只能使用在同步控制塊中;
- sleep 通常被用於暫停執行,wait 通常被用於執行緒間互動/通訊,。
- sleep 必須捕獲異常,而 wait,notify 和 notifyAll 不需要捕獲異常;
- sleep(milliseconds)可以用時間指定來使它自動醒過來,如果時間不到你只能呼叫 interreput()來強行打斷;wait()可以用 notify()直接喚起。
16、鎖池和等待池的概念
鎖池:假設執行緒 A 已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個 synchronized 方法(或者 synchronized 塊),由於這些執行緒在進入物件的 synchronized 方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒 A 擁有,所以這些執行緒就進入了該物件的鎖池中。
等待池:假設一個執行緒 A 呼叫了某個物件的 wait()方法,執行緒 A 就會釋放該物件的鎖,之後進入到了該物件的等待池中。
17、notify()和 notifyAll()有什麼區別?
- 如果執行緒呼叫了物件的 wait()方法,那麼執行緒便會處於該物件的等待池中,等待池中的執行緒不會去競爭該物件的鎖。
- 當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖。也就是說,呼叫了 notify 後只有一個執行緒會由等待池進入鎖池,而 notifyAll 會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭。
- 優先順序高的執行緒競爭到物件鎖的概率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中。而競爭到物件鎖的執行緒則繼續往下執行,直到執行完了 synchronized 程式碼塊,它會釋放掉該物件鎖,這時鎖池中的執行緒會繼續競爭該物件鎖。
18、Java 中的 main 執行緒是不是最後一個退出的執行緒?
- JVM 會在所有的非守護執行緒(使用者執行緒)執行完畢後退出;
- main 執行緒是使用者執行緒;
- 僅有 main 執行緒一個使用者執行緒執行完畢,不能決定 JVM 是否退出,也即是說 main 執行緒並不一定是最後一個退出的執行緒。
19、執行緒的 run()和 start()有什麼區別?
run()方法:
方法 run()稱為執行緒體,可以重複多次呼叫;如果直接呼叫 run(),其實就相當於是呼叫了一個普通函式而已。
start()方法:
用啟動一個執行緒,真正實現了多執行緒執行。不能多次啟動同一個執行緒;
ThreadTest {
static ThreadDemo extends Thread{
@Override
run() {
for (int i = 0; i < 100; i++) {
System.out.println("This is a Thread test"+i);
}
System.out.println("Thread Priority:"+this.getPriority());
}
}
//觀察直接呼叫run()和用start()啟動一個執行緒的差別
main(String[] args) {
Thread thread = new ThreadDemo();
System.out.println("Main Thread Priority = " + Thread.currentThread().getPriority());
//第一種
//表明: run()和其他方法的呼叫沒任何不同,main方法按順序執行了它,並打印出最後一句
// thread.run();
//第二種
//表明: start()方法啟動執行緒,由於main執行緒和thread執行緒都是使用者執行緒(非守護執行緒),且優先順序一致,因此在本程式中main執行緒退出後,
//thread執行緒才進入執行狀態執行程式碼,等所有的使用者執行緒都退出後,jvm才退出。
//main執行緒是使用者執行緒,從main方法中構建的執行緒預設也是使用者執行緒,且優先順序相等
// thread.start();
//第三種
//為什麼沒有打印出100句呢?因為我們將thread執行緒設定為了daemon(守護)執行緒,程式中main執行緒退出後,只有守護執行緒存在的時候,JVM隨時可以退出,所以隨機列印了幾句
//2、當java虛擬機器器中有守護執行緒在執行的時候,java虛擬機器器會關閉。當所有常規執行緒執行完畢以後,
//守護執行緒不管執行到哪裡,虛擬機器器都會退出執行。所以你的守護執行緒最好不要寫一些會影響程式的業務邏輯。否則無法預料程式到底會出現什麼問題
// thread.setDaemon(true);
//第四種
//使用者執行緒可以被System.exit(0)強制kill掉,JVM便可以退出,所以隨機列印了幾句
thread.start();
System.out.println("main thread is over");
System.exit(1);
}
}
複製程式碼
20、什麼是死鎖(deadlock)?
死鎖 :是指兩個或兩個以上的程式在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。
(1) 因為系統資源不足。
(2) 程式執行推進順序不合適。
(3) 資源分配不當等。
死鎖產生的4個必要條件:
- 互斥條件:程式要求對所分配的資源(如印表機)進行排他性控制,即在一段時間內某資源僅為一個程式所佔有。此時若有其他程式請求該資源,則請求程式只能等待。
- 不剝奪條件:程式所獲得的資源在未使用完畢之前,不能被其他程式強行奪走,即只能由獲得該資源的程式自己來釋放(只能是主動釋放)。
- 請求和保持條件:程式已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他程式佔有,此時請求程式被阻塞,但對自己已獲得的資源保持不放。
- 迴圈等待條件:存在一種程式資源的迴圈等待鏈,鏈中每一個程式已獲得的資源同時被鏈中下一個程式所請求。
21、如何確保N個執行緒可以訪問N個資源同時又不導致死鎖?
使用多執行緒的時候,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序,並強制執行緒按照指定的順序獲取鎖。因此,如果所有的執行緒都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了。