|
在編程工作中,我們經(jīng)常會用到或聽到多線程三個字,多線程編程的好處就是可以讓多個任務(wù)進(jìn)行并發(fā),從而更加充分利用CPU,減少CPU的無效等待時間。
多線程的執(zhí)行流程圖如下:

接下來我們會映照上圖介紹多線程執(zhí)行過程中經(jīng)歷的五種狀態(tài):
1. 新建狀態(tài):
新建狀態(tài)就是我們通過new關(guān)鍵字實例化出一個線程類的對象時的狀態(tài)。
public class IsAThread extends Thread{ System.out.println("這是一個線程類");
public static void main(String[] args) { IsAThread isAThread = new IsAThread();
此時,我們就說 isAThread 這個線程對象進(jìn)入了新建狀態(tài)。
2. 可運行狀態(tài):
當(dāng)我們調(diào)用了新建狀態(tài)下的線程對象的 start() 方法來啟動這個線程,并且線程對象已經(jīng)準(zhǔn)備好了除CPU時間片段之外的所有資源后,該線程對象會被放入“可運行線程池”中等待CPU分配時間片段給自身。在自身獲得CPU的時間片段之后便會執(zhí)行自身 run() 方法中定義的邏輯,示例中的線程對象的 run() 方法是打印了 “這是一個線程類” 這么一句話到控制臺。
public static void main(String[] args) { IsAThread isAThread = new IsAThread();
3. 運行狀態(tài):
運行狀態(tài)的線程在分配到CPU的時間片段之后,便會真正開始執(zhí)行線程對象 run() 方法中定義的邏輯代碼了,示例中的線程對象的 run() 方法是打印了 “這是一個線程類” 這么一句話到控制臺。
1)但是生產(chǎn)環(huán)境中的線程對象的 run() 方法一般不會這么簡單,可能業(yè)務(wù)代碼邏輯復(fù)雜,造成CPU的時間片段所規(guī)定的時長已經(jīng)用完之后,業(yè)務(wù)代碼還沒執(zhí)行完;
2)或者是當(dāng)前線程主動調(diào)用了Thread.yield()方法來讓出自身的CPU時間片段。
public class IsAThread extends Thread{ // 主動讓出自身獲取到的CPU時間片段給其他線程使用 System.out.println("這是一個線程類");
此時,運行狀態(tài)會轉(zhuǎn)回可運行狀態(tài),等待下一次分配到CPU時間片段之后繼續(xù)執(zhí)行未完成的操作。
4. 阻塞狀態(tài):
阻塞狀態(tài)指的是運行狀態(tài)中的線程因為某種原因主動放棄了自己的CPU時間片段來讓給其他線程使用,可能的阻塞類型及原因有:
4.1 等待阻塞:
線程被調(diào)用了 Object.wait() 方法后會立刻釋放掉自身獲取到的鎖并進(jìn)入“等待池”進(jìn)行等待,等待池中的線程被其他線程調(diào)用了 Object.notify() 或 Object.notifyAll() 方法后會被喚醒從而從“等待池”進(jìn)入到“等鎖池”,“等鎖池”中的線程在重新獲取到鎖之后會轉(zhuǎn)為可運行狀態(tài)。
值得注意的是:wait()和notify()/notifyAll()只能用在被synchronized包含的代碼塊中,而說明中的Object.wait和Object.notify的這個Object實際上是指作為synchronized鎖的對象。
例如:
我們創(chuàng)建兩個線程類,StringBufferThread和StringBufferThread2,這兩個類唯一的不同就是run()方法的實現(xiàn)。
StringBufferThread:
import java.util.concurrent.CountDownLatch; public class StringBufferThread implements Runnable { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread2\n"); countDownLatch.countDown();
main:
public static void main(String[] args) throws InterruptedException { StringBuffer tipStr = new StringBuffer(); // 使用CountDownLatch保證子線程全部執(zhí)行完成后主線程才打印結(jié)果 CountDownLatch countDownLatch = new CountDownLatch(2); StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch); StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch); Thread thread1 = new Thread(stringBufferThread); Thread thread2 = new Thread(stringBufferThread2); 為了保證先讓thread1執(zhí)行,我們讓thread1執(zhí)行后主線程睡眠5秒鐘再執(zhí)行thread2, 如果不進(jìn)行睡眠的話我們無法控制CPU分配時間片段,有可能直接就先分配給thread2線程了, 這樣就會造成thread2先于thread1執(zhí)行 // 調(diào)用countDownLatch.await()保證子線程全部執(zhí)行完后主線程才繼續(xù)執(zhí)行 System.out.println(tipStr.toString());
那么我們先來看一下這種沒使用wait()和notify()的情形下,先后執(zhí)行這兩個線程對象時的結(jié)果:

跟邏輯一樣,先執(zhí)行了stringBufferThread然后執(zhí)行了stringBufferThread2。
接下來,修改StringBufferThread類:
import java.util.concurrent.CountDownLatch; public class StringBufferThread implements Runnable { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { 在將字符串追加到StringBuffer前,調(diào)用鎖對象StringBufferThread這個類的wait(), StringBufferThread.class.wait(); } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
為StringBufferThread類中的run方法中,將字符串"This is StringBufferThread1\n"加入StringBuffer對象之前,加入wait()方法來進(jìn)行等待,注意,wait()方法會立刻釋放掉自身的鎖后,也就是其他爭取到鎖的線程可以運行被這個synchronized保護的代碼塊了。
隨后,我們修改StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread2\n"); 在將字符串追加到StringBuffer后,調(diào)用鎖對象StringBufferThread這個類的notify(), 來喚醒本這個鎖對象的wait()方法等待的子線程,本例中就是main方法中的stringBufferThread這個子線程 StringBufferThread.class.notify(); countDownLatch.countDown();
也就是在字符串"This is StringBufferThread2\n"追加到StringBuffer之后調(diào)用了 notify() 方法來喚醒被 StringBufferThread.class 這個鎖等待的線程,本例中就是main方法中的stringBufferThread這個子線程,本喚醒的子線程會進(jìn)入等鎖池,等待重新爭取到鎖之后,會繼續(xù)執(zhí)行代碼。
main方法不變,我們來看看執(zhí)行結(jié)果:

與我們預(yù)想的一樣,因為thread1在追加字符串到StringBuffer對象之前調(diào)用了鎖對象的wait(),就立即釋放掉了自身獲取到的鎖并進(jìn)入等待池中了,這時thread2獲取了鎖,將字符串"This is StringBufferThread2\n"首先追加到了StringBuffer對象的開頭,然后調(diào)用鎖對象的notify()方法喚醒了thread1,被喚醒的thread1重新獲取鎖之后,才將自身的字符串"This is StringBufferThread1\n"追加到了StringBuffer對象的末尾。
4.2 同步阻塞:
線程執(zhí)行到了被 synchronized 關(guān)鍵字保護的同步代碼時,如果此時鎖已經(jīng)被其他線程取走,則該線程會進(jìn)入到“等鎖池”,直到持有鎖的那個線程釋放掉鎖并且自身獲取到鎖之后,自身會轉(zhuǎn)為可運行狀態(tài)。
例子如下:
StringBufferThread:
import java.util.concurrent.CountDownLatch; public class StringBufferThread implements Runnable { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { // 睡眠10秒,因為主線程在調(diào)用本線程5秒后就會調(diào)用第二個子線程,多睡眠5秒,就能看出效果 } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread這個類作為鎖 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread2\n"); countDownLatch.countDown();
main方法不變:
public static void main(String[] args) throws InterruptedException { StringBuffer tipStr = new StringBuffer(); // 使用CountDownLatch保證子線程全部執(zhí)行完成后主線程才打印結(jié)果 CountDownLatch countDownLatch = new CountDownLatch(2); StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch); StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch); Thread thread1 = new Thread(stringBufferThread); Thread thread2 = new Thread(stringBufferThread2); 為了保證先讓thread1執(zhí)行,我們讓thread1執(zhí)行后主線程睡眠5秒鐘再執(zhí)行thread2, 如果不進(jìn)行睡眠的話我們無法控制CPU分配時間片段,有可能直接就先分配給thread2線程了, 這樣就會造成thread2先于thread1執(zhí)行 // 調(diào)用countDownLatch.await()保證子線程全部執(zhí)行完后主線程才繼續(xù)執(zhí)行 System.out.println(tipStr.toString());
執(zhí)行結(jié)果如下:

由此可見,主線程調(diào)用thread1后的5秒后調(diào)用了thread2,thread1在執(zhí)行時首先拿走了鎖對象并睡眠了10秒,在這10秒鐘,thread2有5秒的時間(10秒減去主線程等待的5秒)去執(zhí)行run方法中的字符串追加操作,但是因為鎖已經(jīng)被thread1拿走了,所以thread2在這漫長的5秒鐘之內(nèi)什么都做不了,只能等待thread1將字符串"This is StringBufferThread1\n"先追加到StringBuffer的開頭,然后才能把自己的字符串"This is StringBufferThread2\n"追加到StringBuffer的末尾。
4.3 其他阻塞:
1)線程中執(zhí)行了 Thread.sleep(xx) 方法進(jìn)行休眠會進(jìn)入阻塞狀態(tài),直到Thread.sleep(xx)方法休眠的時間超過參數(shù)設(shè)定的時間而超時后線程會轉(zhuǎn)為可運行狀態(tài)。Thread.sleep(xx)方法的使用在本文很多例子都體現(xiàn)了,就不演示了。
2)線程ThreadA中調(diào)用了ThreadB.join()方法來等待ThreadB線程執(zhí)行完畢,從而ThreadA進(jìn)入阻塞狀態(tài),直到ThreadB線程執(zhí)行完畢后ThreadA會轉(zhuǎn)為可運行狀態(tài)。
例子如下:
StringBufferThread:
import java.util.concurrent.CountDownLatch; public class StringBufferThread implements Runnable { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch, Thread thread2) { this.countDownLatch = countDownLatch; // 這里阻塞住,等待thread2執(zhí)行完畢才會繼續(xù)向下執(zhí)行 } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable { CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; thread2睡眠3秒,就能看出效果,如果join()失效, 那么StringBuffer中一定是"This is StringBufferThread1\n"開頭的 } catch (InterruptedException e) { sb.append("This is StringBufferThread2\n"); countDownLatch.countDown();
隨后,修改main方法:
public static void main(String[] args) throws InterruptedException { StringBuffer tipStr = new StringBuffer(); // 使用CountDownLatch保證子線程全部執(zhí)行完成后主線程才打印結(jié)果 CountDownLatch countDownLatch = new CountDownLatch(2); StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch); Thread thread2 = new Thread(stringBufferThread2); StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch, thread2); Thread thread1 = new Thread(stringBufferThread); // 調(diào)用countDownLatch.await()保證子線程全部執(zhí)行完后主線程才繼續(xù)執(zhí)行 System.out.println(tipStr.toString());
執(zhí)行結(jié)果如下:

由此可見,雖然thread1先于thread2執(zhí)行,但是因為在將字符串追加到StringBuffer對象前調(diào)用了thread2.join(),便被阻塞住了,此時thread2睡眠三秒后,將字符串"This is StringBufferThread2\n"追加到了StringBuffer對象的開頭,thread2執(zhí)行完畢;隨后因為thread1等待的thread2已經(jīng)執(zhí)行完畢了,thread1便由阻塞狀態(tài)轉(zhuǎn)為可運行狀態(tài),在分配到CPU的時間片段后,便將字符串"This is StringBufferThread1\n"追加到了StringBuffer對象的結(jié)尾。
3)線程中進(jìn)行了I/O操作,I/O操作在輸入輸出行為執(zhí)行完畢之前都不會返回給調(diào)用者任何結(jié)果,直到I/O操作執(zhí)行完畢之后線程會轉(zhuǎn)為可運行狀態(tài)。
例如:
我們編寫ThreadTest類:
import java.util.Scanner; public class ThreadTest implements Runnable { System.out.println("This is StringBufferThread1 Begin\n"); Scanner scanner = new Scanner(System.in); System.out.println("請輸入內(nèi)容:"); // 線程會阻塞在這,等待用戶在控制臺輸入數(shù)據(jù)后繼續(xù)執(zhí)行 String content = scanner.nextLine(); System.out.println("您輸入的內(nèi)容是:" + content + "\n"); System.out.println("This is StringBufferThread1 end\n");
執(zhí)行main方法:
public static void main(String[] args) throws InterruptedException { ThreadTest threadTest = new ThreadTest(); Thread thread1 = new Thread(threadTest);
執(zhí)行效果如下:

線程會阻塞在這里等待我們從控制臺輸入內(nèi)容。

輸入內(nèi)容后,線程繼續(xù)運行。
|