大家好,我是小黑,一個在網際網路"苟且偷生"的農民工。點贊再看,養成習慣呀。
微信搜尋【小黑說Java】有我的所有文章。
前段時間公司面試招人,發現好多小夥伴雖然已經有兩三年的工作經驗,但是對於一些Java基礎的知識掌握的都不是很紮實,所以小黑決定開始跟大家分享一些Java基礎相關的內容。首先這一期我們從Java的多執行緒開始。
好了,接下來進入正題,先來看看什麼是程序和執行緒。
程序VS執行緒
程序是計算機作業系統中的一個執行緒集合,是系統資源排程的基本單位,正在執行的一個程式,比如QQ,微信,音樂播放器等,在一個程序中至少包含一個執行緒。
執行緒是計算機作業系統中能夠進行運算排程的最小單位。一條執行緒實際上就是一段單一順序執行的程式碼。比如我們音樂播放器中的字幕展示,和聲音的播放,就是兩個獨立執行的執行緒。
瞭解完程序和執行緒的區別,我們再來看一下併發和並行的概念。
併發VS並行
當有多個執行緒在操作時,如果系統只有一個CPU,假設這個CPU只有一個核心,則它根本不可能真正同時進行一個以上的執行緒,它只能把CPU執行時間劃分成若干個時間段,再將時間段分配給各個執行緒執行,在一個時間段的執行緒程式碼執行時,其它執行緒處於掛起狀。這種方式我們稱之為併發(Concurrent)。
當系統有一個以上CPU或者一個CPU有多個核心時,則執行緒的操作有可能非併發。當一個CPU執行一個執行緒時,另一個CPU可以執行另一個執行緒,兩個執行緒互不搶佔CPU資源,可以同時進行,這種方式我們稱之為並行(Parallel)。
讀完上面這段話,是不是感覺好像懂了,又好像沒懂?啥併發?啥並行?馬什麼梅?什麼冬梅?
彆著急,小黑先給大家用個通俗的例子解釋一下併發和並行的區別,然後再看上面這段話,相信大家就都能夠理解了。
你吃飯吃到一半,電話來了,你一直把飯吃完之後再去接電話,這就說明你不支援併發也不支援並行;
你吃飯吃到一半,電話來了,你去電話,然後吃一口飯,接一句電話,吃一口飯,接一句電話,這就說明你支援併發;
你吃飯吃到一半,電話來了,你妹接電話,你在一直吃飯,你妹在接電話,這就叫並行。
總結一下,併發的關鍵,是看你有沒有處理多個任務的能力,不是同時處理;
並行的關鍵是看能不能同時處理多個任務,那要想處理多個任務,就要有“你妹”(另一個CPU或者核心)的存在(怎麼感覺好像在罵人)。
Java中的執行緒
在Java作為一門高階計算機語言,同樣也有程序和執行緒的概念。
我們用Main方法啟動一個Java程式,其實就是啟動了一個Java程序,在這個程序中至少包含2個執行緒,另一個是用來做垃圾回收的GC執行緒。
Java中通常透過Thread類來建立執行緒,接下來我們看看具體是如何來做的。
執行緒的建立方式
要想在Java程式碼中要想自定義一個執行緒,可以透過繼承Thread類,然後建立自定義個類的物件,呼叫該物件的start()方法來啟動。
public class ThreadDemo {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("這是我自定義的執行緒");
}
}
或者實現java.lang.Runnable介面,在建立Thread類的物件時,將自定義java.lang.Runnable介面的例項物件作為引數傳給Thread,然後呼叫start()方法啟動。
public class ThreadDemo {
public static void main(String[] args) {
new Thread(new MyRunnable()).s
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("這是我自定義的執行緒");
}
}
那在實際開發過程中,是建立Thread的子類,還是實現Runnable介面呢?其實並沒有一個確定的答案,我個人更喜歡實現Runnable介面這種用法。在以後要學的執行緒池中也是對於Runnable介面的例項進行管理。當然我們也要根據實際場景靈活變通。
執行緒的啟動和停止
從上面的程式碼中我們其實已經看到,建立執行緒之後透過呼叫start()方法就可以實現執行緒的啟動。
new MyThread().start();
注意,我們看到從上一節的程式碼中看到我們自定義的Thread類是重寫了父類的run()方法,那我們直接呼叫run()方法可不可以啟動一個執行緒呢?答案是不可以。直接呼叫run()方法和普通的方法呼叫沒有區別,不會開啟一個新執行緒執行,這裡一定要注意。
那要怎麼來停止一個執行緒呢?我們看Thread類的方法,是有一個stop()方法的。
@Deprecated // 已經棄用了。
public final void stop() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
if (threadStatus != 0) {
resume();
}
stop0(new ThreadDeath());
}
但是我們從這個方法上可以看到是加了@Deprecated註解的,也就是這個方法被JDK棄用了。被棄用的原因是因為透過stop()方法會強制讓這個執行緒停止,這對於執行緒中正在執行的程式是不安全的,就好比你正在拉屎,別人強制不讓你拉了,這個時候你是夾斷還是不夾斷(這個例子有點噁心,但是很形象哈哈)。所以在需要停止形成的是不不能使用stop方法。
那我們應該怎樣合理地讓一個執行緒停止呢,主要有以下2種方法:
第一種:使用標誌位終止執行緒
class MyRunnable implements Runnable {
private volatile boolean exit = false; // volatile關鍵字,保證主執行緒修改後當前執行緒能夠看到被改後的值(可見性)
@Override
public void run() {
while (!exit) { // 迴圈判斷標識位,是否需要退出
System.out.println("這是我自定義的執行緒");
}
}
public void setExit(boolean exit) {
this.exit = exit;
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
runnable.setExit(true); //修改標誌位,退出執行緒
}
}
線上程中定義一個標誌位,透過判斷標誌位的值決定是否繼續執行,在主執行緒中透過修改標誌位的值達到讓執行緒停止的目的。
第二種:使用interrupt()中斷執行緒
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
Thread.sleep(10);
t.interrupt(); // 企圖讓執行緒中斷
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.println("執行緒正在執行~" + i);
}
}
}
這裡需要注意的點,就是interrupt()方法並不會像使用標誌位或者stop()方法一樣,讓執行緒馬上停止,如果你執行上面這段程式碼會發現,執行緒t並不會被中斷。那麼如何才能讓執行緒t停止呢?這個時候就要關注Thread類的另外兩個方法。
public static boolean interrupted(); // 判斷是否被中斷,並清除當前中斷狀態
private native boolean isInterrupted(boolean ClearInterrupted); // 判斷是否被中斷,透過ClearInterrupted決定是否清楚中斷狀態
那麼我們再來修改一下上面的程式碼。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
Thread.sleep(10);
t.interrupt();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
//if (Thread.currentThread().isInterrupted()) {
if (Thread.interrupted()) {
break;
}
System.out.println("執行緒正在執行~" + i);
}
}
}
這個時候執行緒t就會被中斷執行。
到這裡大家其實會有個疑惑,這種方式和上面的透過標誌位的方式好像沒有什麼區別呀,都是判斷一個狀態,然後決定要不要結束執行,它們倆到底有啥區別呢?這裡其實就涉及到另一個東西叫做執行緒狀態,如果當執行緒t在sleep()或者wait()的時候,如果用標識位的方式,其實並不能立馬讓執行緒中斷,只能等sleep()結束或者wait()被喚醒之後才能中斷。但是用第二種方式,線上程休眠時,如果呼叫interrupt()方法,那麼就會丟擲一個異常InterruptedException,然後執行緒繼續執行。
執行緒的狀態
透過上面對於執行緒停止方法的對比,我們瞭解到執行緒除了執行和停止這兩種狀態意外,還有wait(),sleep()這樣的方法,可以讓執行緒進入到等待或者休眠的狀態,那麼執行緒具體都哪些狀態呢?其實透過程式碼我們能夠找到一些答案。在Thread類中有一個叫State的列舉類,這個列舉類中定義了執行緒的6中狀態。
public enum State {
/**
* 尚未啟動的執行緒的執行緒狀態
*/
NEW,
/**
* 可執行狀態
*/
RUNNABLE,
/**
* 阻塞狀態
*/
BLOCKED,
/**
* 等待狀態
*/
WAITING,
/**
* 超時等待狀態
*/
TIMED_WAITING,
/**
* 終止狀態
*/
TERMINATED;
}
那麼執行緒中的這六種狀態到底是怎麼變化的呢?什麼時候時RUNNABLE,什麼時候BLOCKED,我們透過下面的圖來展示執行緒見狀態發生變化的情況。
執行緒狀態詳細說明
初始化狀態(NEW)
在一個Thread例項被new出來時,這個執行緒物件的狀態就是初始化(NEW)狀態。
可執行狀態(RUNNABLE)
- 在呼叫start()方法後,這個執行緒就到達可執行狀態,注意,可執行狀態並不代表一定在執行,因為作業系統的CPU資源要輪換執行(也就是最開始說的併發),要等作業系統排程,只有被排程到才會開始執行,所以這裡只是到達就緒(READY)狀態,說明有資格被系統排程;
- 當系統排程本執行緒之後,本執行緒會到達執行中(RUNNING)狀態,在這個狀態如果本執行緒獲取到的CPU時間片用完以後,或者呼叫yield()方法,會重新進入到就緒狀態,等待下一次被排程;
- 當某個休眠執行緒被notify(),會進入到就緒狀態;
- 被park(Thread)的執行緒又被unpark(Thread),會進入到就緒狀態;
- 超時等待的執行緒時間到時,會進入到就緒狀態;
- 同步程式碼塊或同步方法獲取到鎖資源時,會進入到就緒狀態;
超時等待(TIMED_WAITING)
當執行緒呼叫sleep(long),join(long)等方法,或者同步程式碼中鎖物件呼叫wait(long),以及LockSupport.arkNanos(long),LockSupport.parkUntil(long)這些方法都會讓執行緒進入超時等待狀態。
等待(WAITING)
等待狀態和超時等待狀態的區別主要是沒有指定等待多長的時間,像Thread.join(),鎖物件呼叫wait(),LockSupport.park()等這些方法會讓執行緒進入等待狀態。
阻塞(BLOCKED)
阻塞狀態主要發生在獲取某些資源時,在獲取成功之前,會進入阻塞狀態,知道獲取成功以後,才會進入可執行狀態中的就緒狀態。
終止(TERMINATED)
終止狀態很好理解,就是當前執行緒執行結束,這個時候就進入終止狀態。這個時候這個執行緒物件也許是存活的,但是沒有辦法讓它再去執行。所謂“執行緒”死不能復生。
執行緒重要的方法
從上一節我們看到執行緒狀態之間變化會有很多方法的呼叫,像Join(),yield(),wait(),notify(),notifyAll(),這麼多方法,具體都是什麼作用,我們來看一下。
上面我們講到過的start()、run()、interrupt()、isInterrupted()、interrupted()這些方法想必都已經理解了,這裡不做過多的贅述。
/**
* sleep()方法是讓當前執行緒休眠若干時間,它會丟擲一個InterruptedException中斷異常。
* 這個異常不是執行時異常,必須捕獲且處理,當執行緒在sleep()休眠時,如果被中斷,這個異常就會產生。
* 一旦被中斷後,丟擲異常,會清除標記位,如果不加處理,下一次迴圈開始時,就無法捕獲這個中斷,故一般在異常處理時再設定標記位。
* sleep()方法不會釋放任何物件的鎖資源。
*/
public static native void sleep(long millis) throws InterruptedException;
/**
* yield()方法是個靜態方法,一旦執行,他會使當前執行緒讓出CPU。讓出CPU不代表當前執行緒不執行了,還會進行CPU資源的爭奪。
* 如果一個執行緒不重要或優先順序比較低,可以用這個方法,把資源給重要的執行緒去做。
*/
public static native void yield();
/**
* join()方法表示無限的等待,他會一直阻塞當前執行緒,只到目標執行緒執行完畢。
*/
public final void join() throws InterruptedException ;
/**
* join(long millis) 給出了一個最大等待時間,如果超過給定的時間目標執行緒還在執行,當前執行緒就不等了,繼續往下執行。
*/
public final synchronized void join(long millis) throws InterruptedException ;
以上這些方法是Thread類中的方法,從方法簽名可以看出,sleep()和yield()方法是靜態方法,而join()方法是成員方法。
而wait(),notify(),notifyAll()這三個方式是Object類中的方法,這三個方法主要用於在同步方法或同步程式碼塊中,用於對共享資源有競爭的執行緒之間的通訊。
/**
* 使當前執行緒等待,直到另一個執行緒呼叫該物件的 notify()方法或 notifyAll()方法。
*/
public final void wait() throws InterruptedException
/**
* 喚醒正在等待物件監視器的單個執行緒。
*/
public final native void notify();
/**
* 喚醒正在等待物件監視器的所有執行緒。
*/
public final native void notifyAll();
針對wait(),notify/notifyAll() 有一個典型的案例:生產者消費者,透過這個案例能加深大家對於這三個方法的印象。
場景如下:
假設現在有一個KFC(KFC給你多少錢,我金拱門出雙倍),裡面有漢堡在銷售,為了漢堡的新鮮呢,店員在製作時最多不會製作超過10個,然後會有顧客來購買漢堡。當漢堡數量到10個時,店員要停止製作,而當數量等於0也就是賣完了的時候,顧客得等新漢堡製作處理。
我們現在透過兩個執行緒一個來製作,一個來購買,來模擬這個場景。程式碼如下:
class KFC {
// 漢堡數量
int hamburgerNum = 0;
public void product() {
synchronized (this) {
while (hamburgerNum == 10) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產一個漢堡" + (++hamburgerNum));
this.notifyAll();
}
}
public void consumer() {
synchronized (this) {
while (hamburgerNum == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("賣出一個漢堡" + (hamburgerNum--));
this.notifyAll();
}
}
}
public class ProdConsDemo {
public static void main(String[] args) {
KFC kfc = new KFC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店員").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consumer();
}
}, "顧客").start();
}
}
從上面的程式碼可以看出,這三個方法是要配合使用的。
wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。
wait()使當前執行緒阻塞,前提是必須先獲得鎖,一般配合synchronized關鍵字使用。
當執行緒執行wait()方法時,會釋放當前的鎖,然後讓出CPU,進入等待狀態。
由於 wait()、notify/notifyAll() 在synchronized 程式碼塊執行,說明當前執行緒一定是獲取了鎖的。只有當notify/notifyAll()被執行時,才會喚醒一個或多個正處於等待狀態的執行緒,然後繼續往下執行,直到執行完synchronized程式碼塊的程式碼或是中途遇到wait() ,再次釋放鎖。
要注意,notify/notifyAll()喚醒沉睡的執行緒後,執行緒會接著上次的執行繼續往下執行。所以在進行條件判斷時候,不能使用if來判斷,假設存在多個顧客來購買,當被喚醒之後如果不做判斷直接去買,有可能已經被另一個顧客買完了,所以一定要用while判斷,在被喚醒之後重新進行一次判斷。
最後再強調一下wait()和我們上面講到的sleep()的區別,sleep()可以隨時隨地執行,不一定在同步程式碼塊中,所以在同步程式碼塊中呼叫也不會釋放鎖,而wait()方法的呼叫必須是在同步程式碼中,並且會釋放鎖。
好了,今天的內容就到這裡。我是小黑,我們下期見。