大家好,我是小黑,一個在網際網路苟且偷生的農民工。前段時間公司面試招人,發現好多小夥伴雖然已經有兩三年的工作經驗,但是對于一些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()方法的調用必須是在同步代碼中,并且會釋放鎖。
好了,今天的内容就到這裡。我是小黑,我們下期見。
如果喜歡小黑也可以關注我的微信公衆号【小黑說Java】。