作為一個 Java開發人員,多執行緒是一個逃不掉的話題,不管是工作還是面試,但理解起來比較模糊難懂,因為多執行緒程式在跑起來的時候比較難於觀察和跟蹤。搞懂多執行緒並行知識,可以在面試的時候和周圍人拉開差距,另外自己在編碼的時候可以做到心中有數。
另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,我認為對面試來說是非常有用的,想要資料的話請點795983544 暗號CSDN。
(1)繼承 Thread 類;
(2)實現 Runnable 介面;
(3)實現 Callable 介面通過 FutureTask 包裝器來建立 Thread 執行緒;
(4)使用 ExecutorService、Callable、Future 實現有返回結果的多執行緒(也就是使用了 ExecutorService 來管理前面的三種方式)。
(1)使用退出標誌,使執行緒正常退出,也就是當 run 方法完成後執行緒終止。
(2)使用 stop 方法強行終止,但是不推薦這個方法,因為 stop 和 suspend 及 resume 一樣都是過期作廢的方法。
(3)使用 interrupt 方法中斷執行緒。
class MyThread extends Thread {
volatile Boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
}
catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true;
// 在例外處理程式碼中修改共用變數的狀態
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
m1.interrupt();
// 阻塞時退出阻塞狀態
Thread.sleep(3000);
// 主執行緒休眠 3 秒以便觀察執行緒 m1 的中斷情況
System.out.println("Stopping application...");
}
}
notify 可能會導致死鎖,而 notifyAll 則不會
任何時候只有一個執行緒可以獲得鎖,也就是說只有一個執行緒可以執行 synchronized 中的程式碼使用 notifyall,可以喚醒所有處於 wait 狀態的執行緒,使其重新進入鎖的爭奪佇列中,而 notify 只能喚醒一個。
wait() 應配合 while 迴圈使用,不應使用 if,務必在 wait()呼叫前後都檢查條件,如果不滿足,必須呼叫 notify()喚醒另外的執行緒來處理,自己繼續 wait()直至條件滿足再往下執行。
notify() 是對 notifyAll()的一個優化,但它有很精確的應用場景,並且要求正確使用。不然可能導致死鎖。正確的場景應該是 WaitSet 中等待的是相同的條件,喚醒任一個都能正確處理接下來的事項,如果喚醒的執行緒無法正確處理,務必確保繼續 notify()下一個執行緒,並且自身需要重新回到 WaitSet 中。
對於 sleep()方法,我們首先要知道該方法是屬於 Thread 類中的。而 wait()方法,則是屬於 Object 類中
的。
sleep()方法導致了程式暫停執行指定的時間,讓出 cpu 該其他執行緒,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復執行狀態。在呼叫 sleep()方法的過程中,執行緒不會釋放物件鎖。
當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件呼叫 notify()方法後本執行緒才進入物件鎖定池準備,獲取物件鎖進入執行狀態。
一旦一個共用變數(類的成員變數、類的靜態成員變數)被 volatile 修飾之後,那麼就具備了兩層語意:
(1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的,volatile 關鍵字會強制將修改的值立即寫入主記憶體。
(2)禁止進行指令重排序。
volatile 不是原子性操作
什麼叫保證部分有序性?
當程式執行到 volatile 變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
x = 2;//語句 1
y = 0;//語句 2
flag = true;//語句 3
x = 4;//語句 4
y = -1;//語句 5
由於flag 變數為 volatile 變數,那麼在進行指令重排序的過程的時候,不會將語句 3 放到語句 1、語句 2 前面,也不會講語句 3 放到語句 4、語句 5 後面。但是要注意語句 1 和語句 2 的順序、語句 4 和語句 5 的順序是不作任何保證的。
使用 Volatile 一般用於 狀態標記量 和 單例模式的雙檢鎖
start()方法被用來啟動新建立的執行緒,而且 start()內部呼叫了 run()方法,這和直接呼叫 run()方法的效果不一樣。當你呼叫 run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒。
明顯的原因是 JAVA 提供的鎖是物件級的而不是執行緒級的,每個物件都有鎖,通過執行緒獲得。如果執行緒需要等待某些鎖那麼呼叫物件中的 wait()方法就有意義了。如果 wait()方法定義在 Thread 類中,執行緒正在等待的是哪個鎖就不明顯了。簡單的說,由於 wait,notify 和 notifyAll 都是鎖級別的操作,所以把他們定義在 Object 類中因為鎖屬於物件。
(1)只有在呼叫執行緒擁有某個物件的獨佔鎖時,才能夠呼叫該物件的 wait(),notify()和 notifyAll()方法。
(2)如果你不這麼做,你的程式碼會丟擲 IllegalMonitorStateException 異常。
(3)還有一個原因是為了避免 wait 和 notify 之間產生競態條件。
wait()方法強制當前執行緒釋放物件鎖。這意味著在呼叫某物件的 wait()方法之前,當前執行緒必須已經獲得該物件的鎖。因此,執行緒必須在某個物件的同步方法或同步程式碼塊中才能呼叫該物件的 wait()方法。
在呼叫物件的 notify()和 notifyAll()方法之前,呼叫執行緒必須已經得到該物件的鎖。因此,必須在某個物件的同步方法或同步程式碼塊中才能呼叫該物件的 notify()或 notifyAll()方法。
呼叫 wait()方法的原因通常是,呼叫執行緒希望某個特殊的狀態(或變數)被設定之後再繼續執行。呼叫 notify()或 notifyAll()方法的原因通常是,呼叫執行緒希望告訴其他等待中的執行緒:「特殊狀態已經被設定」。這個狀態作為執行緒間通訊的通道,它必須是一個可變的共用狀態(或變數)。
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java 多執行緒的中斷機制是用內部標識來實現的,呼叫 Thread.interrupt()來中斷一個執行緒就會設定中斷標識為 true。當中斷執行緒呼叫靜態方法 Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法 isInterrupted()用來查詢其它執行緒的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何丟擲 InterruptedException 異常的方法都會將中斷狀態清零。無論如何,一個執行緒的中斷狀態有有可能被其它執行緒呼叫中斷來改變。
相似點:
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他存取該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的。
區別:
這兩種方式最大區別就是對於 Synchronized 來說,它是 java 語言的關鍵字,是原生語法層面的互斥,需要 jvm 實現。而 ReentrantLock 它是 JDK 1.5 之後提供的 API 層面的互斥鎖,需要 lock()和 unlock()方法配合 try/finally 語句塊來完成。
Synchronized 進過編譯,會在同步塊的前後分別形成 monitorenter 和 monitorexit 這個兩個位元組碼指令。在執行 monitorenter 指令時,首先要嘗試獲取物件鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件鎖,把鎖的計算器加 1,相應的,在執行 monitorexit 指令時會將鎖計算器就減 1,當計算器為 0 時,鎖就被釋放了。如果獲取物件鎖失敗,那當前執行緒就要阻塞,直到物件鎖被另一個執行緒釋放為止。
由於 ReentrantLock 是 java.util.concurrent 包下提供的一套互斥鎖,相比 Synchronized,ReentrantLock 類提供了一些高階功能,主要有以下 3 項:
(1)等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於 Synchronized 來說可以避免出現死鎖的情況。
(2)公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized 鎖非公平鎖,ReentrantLock 預設的建構函式是建立的非公平鎖,可以通過引數 true 設為公平鎖,但公平鎖表現的效能不是很好。
(3)鎖繫結多個條件,一個 ReentrantLock 物件可以同時繫結對個物件。
在多執行緒中有多種方法讓執行緒按特定順序執行,你可以用執行緒類的 join()方法在一個執行緒中啟動另一個執行緒,另外一個執行緒完成該執行緒繼續執行。為了確保三個執行緒的順序你應該先啟動最後一個(T3 呼叫 T2,T2 呼叫 T1),這樣 T1 就會先完成而 T3 最後完成。
實際上先啟動三個執行緒中哪一個都行,因為在每個執行緒的 run 方法中用 join 方法限定了三個執行緒的執行順序。
public class JoinTest2 {
// 1.現在有 T1、T2、T3 三個執行緒,你怎樣保證 T2 在 T1 執行完後執行,T3 在 T2 執行完後執行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
}
);
@Override
public void run() {
try {
// 參照 t1 執行緒,等待 t1 執行緒執行完
t1.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
}
);
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 參照 t2 執行緒,等待 t2 執行緒執行完
t2.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
}
);
t3.start();
//這裡三個執行緒的啟動順序可以任意,大家可以試下!
t2.start();
t1.start();
}
}
SynchronizedMap()和 Hashtable 一樣,實現上在呼叫 map 所有方法時,都對整個 map 進行同步。而 ConcurrentHashMap 的實現卻更加精細,它對 map 中的所有桶加了鎖。所以,只要有一個執行緒存取 map,其他執行緒就無法進入 map,而如果一個執行緒在存取 ConcurrentHashMap 某個桶時,其他執行緒,仍然可以對 map 執行某些操作。
所以,ConcurrentHashMap 在效能以及安全性方面,明顯比 Collections.synchronizedMap()更加有優勢。同時,同步操作精確控制到桶,這樣,即使在遍歷 map 時,如果其他執行緒試圖對 map 進行資料修改,也不會丟擲 ConcurrentModificationException。
執行緒安全就是說多執行緒存取同一程式碼,不會產生不確定的結果。
在多執行緒環境中,當各執行緒不共用資料的時候,即都是私有(private)成員,那麼一定是執行緒安全的。但這種情況並不多見,在多數情況下需要共用資料,這時就需要進行適當的同步控制了。
執行緒安全一般都涉及到 synchronized, 就是一段程式碼同時只能有一個執行緒來操作 不然中間過程可能會產生不可預製的結果。
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行的 ArrayList 不是執行緒安全的。
Yield 方法可以暫停當前正在執行的執行緒物件,讓其它有相同優先順序的執行緒執行。它是一個靜態方法而且只保證當前執行緒放棄 CPU 佔用而不能保證使其它執行緒一定能佔用 CPU,執行 yield()的執行緒有可能在進入到暫停狀態後馬上又被執行。
兩個方法都可以向執行緒池提交任務,execute()方法的返回型別是 void,它定義在 Executor 介面中, 而 submit()方法可以返回持有計算結果的 Future 物件,它定義在 ExecutorService 介面中,它擴充套件了 Executor 介面,其它執行緒池類像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有這些方法。
synchronized 關鍵字解決的是多個執行緒之間存取資源的同步性,synchronized 關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。
另外,在 Java 早期版本中,synchronized 屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的,Java 的執行緒是對映到作業系統的原生執行緒之上的。如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對 synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6 對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
synchronized 關鍵字最主要的三種使用方式:
(1)修飾實體方法: 作用於當前物件範例加鎖,進入同步程式碼前要獲得當前物件範例的鎖
(2)修飾靜態方法: 也就是給當前類加鎖,會作用於類的所有物件範例,因為靜態成員不屬於任何一個範例物件,是類成員( static 表明這是該類的一個靜態資源,不管 new 了多少個物件,只有一份)。所以如果一個執行緒 A 呼叫一個範例物件的非靜態 synchronized 方法,而執行緒 B 需要呼叫這個範例物件所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為存取靜態 synchronized 方法佔用的鎖是當前類的鎖,而存取非靜態 synchronized 方法佔用的鎖是當前範例物件鎖。
(3)修飾程式碼塊: 指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實體方法上是給物件範例上鎖。儘量不要使用 synchronized(String a) 因為 JVM 中,字串常數池具有快取功能!
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次運
行結果和單執行緒執行的結果是一樣的,而且其他的變數 的值也和預期的是一樣的,就是執行緒安全的。
一旦一個共用變數(類的成員變數、類的靜態成員變數)被 volatile 修飾之後,那麼就具備了兩層語意:
(1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
(2)禁止進行指令重排序。
(3)volatile 本質是在告訴 jvm 當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主記憶體中讀取;synchronized 則是鎖定當前變數,只有當前執行緒可以存取該變數,其他執行緒被阻塞住。
(4)volatile 僅能使用在變數級別;synchronized 則可以使用在變數、方法、和類級別的。
(5)volatile 僅能實現變數的修改可見性,並不能保證原子性;synchronized 則可以保證變數的修改可見性和原子性。
(6)volatile 不會造成執行緒的阻塞;synchronized 可能會造成執行緒的阻塞。
(7)volatile 標記的變數不會被編譯器優化;synchronized 標記的變數可以被編譯器優化。
(1)newSingleThreadExecutor:建立一個單執行緒的執行緒池,此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
(2)newFixedThreadPool:建立固定大小的執行緒池,每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。
(3)newCachedThreadPool:建立一個可快取的執行緒池,此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說 JVM)能夠建立的最大執行緒大小。
(4)newScheduledThreadPool:建立一個大小無限的執行緒池,此執行緒池支援定時以及週期性執行任務的需求。
(5)newSingleThreadExecutor:建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
(如果問到了這樣的問題,可以展開的說一下執行緒池如何用、執行緒池的好處、執行緒池的啟動策略)合理利用執行緒池能夠帶來三個好處。
(1)降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
(2)提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
(3)提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。
我們日常的工作中都使用開發工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的偵錯程式,或者是通過打包工具把專案打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常執行了
(1)先把 Java 程式碼編譯成位元組碼,也就是把 .java 型別的檔案編譯成 .class 型別的檔案。這個過程的大致執行流程:Java 原始碼 -> 詞法分析器 -> 語法分析器 -> 語意分析器 -> 字元碼生成器 -> 最終生成位元組碼,其中任何一個節點執行失敗就會造成編譯失敗;
(2)把 class 檔案放置到 Java 虛擬機器器,這個虛擬機器器通常指的是 Oracle 官方自帶的 Hotspot JVM;
(3)Java 虛擬機器器使用類載入器(Class Loader)裝載 class 檔案;
(4)類載入完成之後,會進行位元組碼效驗,位元組碼效驗通過之後 JVM 直譯器會把位元組碼翻譯成機器碼交由作業系統執行。但不是所有程式碼都是解釋執行的,JVM 對此做了優化,比如,以 Hotspot 虛擬機器器來說,它本身提供了 JIT(Just In Time)也就是我們通常所說的動態編譯器,它能夠在執行時將熱點程式碼編譯為機器碼,這個時候位元組碼就變成了編譯執行。Java 程式執行流程圖如下:
多執行緒高並行在一些網際網路大廠是面試必問的一個技術點,所以在面試時一定要注重重點,想一些高並行高可用的技術。面試時要掌握節奏,說一些讓面試官眼前一亮的技術,有些基礎的東西能少說就少說,畢竟面試官面了這麼多早就聽夠了,越是稀少的越是能激發面試官的興趣,然後掌握在自己的節奏中。
另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,我認為對面試來說是非常有用的,想要資料的話請點795983544 暗號CSDN。