1 多線程
我們在之前,學習的程序在沒有跳轉語句的前提下,都是由上至下依次執行,那現在想要設計一個程序,邊打游戲邊聽歌,怎么設計?
要解決上述問題,咱們得使用多進程或者多線程來解決.
1.1 并發與并行
- 并發:指兩個或多個事件在同一個時間段內發生。
- 并行:指兩個或多個事件在同一時刻發生(同時發生)。
[圖片上傳失敗...(image-4afbb6-1580720568911)]
在操作系統中,安裝了多個程序,并發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。
而在多個 CPU 系統中,則這些可以并發執行的程序便可以分配到多個處理器上(CPU),實現多任務并行執行,即利用每個處理器來處理一個可以并發執行的程序,這樣多個程序便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,并行處理的程序越多,能大大的提高電腦運行的效率。
注意:單核處理器的計算機肯定是不能并行的處理多個任務的,只能是多個任務在單個CPU上并發運行。同理,線程也是一樣的,從宏觀角度上理解線程是并行運行的,但是從微觀角度上分析卻是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之為線程調度。
1.2 線程與進程
進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。
-
線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之為多線程程序。
簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程
我們可以再電腦底部任務欄,右鍵----->打開任務管理器,可以查看當前任務的進程:
進程
[圖片上傳失敗...(image-ff8a13-1580720568911)]
線程
[圖片上傳失敗...(image-b72516-1580720568911)]
線程調度:
-
分時調度
所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。
-
搶占式調度
優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。
- 設置線程的優先級
[圖片上傳失敗...(image-77c44e-1580720568911)]
-
搶占式調度詳解
大部分操作系統都支持多進程并發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開著畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行著“。
實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行著高速的切換。對于CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
其實,多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。[圖片上傳失敗...(image-e3f872-1580720568911)]
1.3 使用Thread類創建線程
翻閱API后得知創建線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable接口方式。
Java使用java.lang.Thread
類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。Java中通過繼承Thread類來創建并啟動多線程的步驟如下:
- 定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
- 創建Thread子類的實例,即創建了線程對象
- 調用線程對象的start()方法來啟動該線程
代碼如下:
自定義線程類:
public class MyThread extends Thread {
//定義指定線程名稱的構造方法
public MyThread(String name) {
//調用父類的String參數的構造方法,指定線程的名稱
super(name);
}
/**
* 重寫run方法,完成該線程執行的邏輯
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
測試類:
public class Demo01 {
public static void main(String[] args) {
//創建自定義線程對象
MyThread mt = new MyThread("新的線程!");
//開啟新線程
mt.start();
//在主方法中執行for循環
for (int i = 0; i < 10; i++) {
System.out.println("main線程!"+i);
}
}
}
Thread 類的常用方法:
java.lang.Thread 類的API中定義了有關線程的一些方法,具體如下:
構造方法:
-
public Thread()
:分配一個新的線程對象。 -
public Thread(String name)
:分配一個指定名字的新的線程對象。 -
public Thread(Runnable target)
:分配一個帶有指定目標新的線程對象。 -
public Thread(Runnable target,String name)
:分配一個帶有指定目標新的線程對象并指定名字。
常用方法:
-
public String getName()
:獲取當前線程名稱。 -
public void start()
:導致此線程開始執行; Java虛擬機調用此線程的run方法 -
public void run()
:此線程要執行的任務在此處定義代碼。 -
public static void sleep(long millis)
:使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。 -
public static Thread currentThread()
:返回對當前正在執行的線程對象的引用。
1.4 使用Runnable接口創建線程
采用 java.lang.Runnable 也是非常常見的一種,我們只需要重寫run方法即可。
步驟如下:
- 定義Runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- 創建Runnable實現類的實例,并以此實例作為Thread的target來創建Thread對象,該Thread對象才是真正
的線程對象。 - 調用線程對象的start()方法來啟動線程。
代碼如下:
定義Runnable接口的實現類
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
測試類
public class Demo {
public static void main(String[] args) {
//創建自定義類對象 線程任務對象
MyRunnable mr = new MyRunnable();
//創建線程對象
Thread t = new Thread(mr, "小強");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺財 " + i);
}
}
}
通過實現 Runnable接口,使得該類有了多線程類的特征。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法里面。Thread類實際上也是實現了Runnable接口的類。
在啟動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然后調用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
tips:
Runnable對象僅僅作為Thread對象的target,Runnable實現類里包含的run()方法僅作為線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。
1.5 Thread 和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
總結:
實現Runnable接口比繼承Thread類所具有的優勢:
- 適合多個相同的程序代碼的線程去共享同一個資源。
- 可以避免java中的單繼承的局限性。
- 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
擴充:在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在操作系統中啟動了一個進程。
1.6 匿名內部類方式實現線程的創建
使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。使用匿名內部類的方式實現Thread類和Runnable接口,重寫的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
//1 線程的父類是Thread
// new MyThread().start();
new Thread(){
//重寫run方法,設置線程任務
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"aaa");
}
}
}.start();
//2 線程的接口Runnable
//Runnable r = new RunnableImpl();//多態
Runnable r = new Runnable(){
//重寫run方法,設置線程任務
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"bbb");
}
}
};
new Thread(r).start();
//簡化接口的方式
new Thread(new Runnable(){
//重寫run方法,設置線程任務
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"ccc");
}
}
}).start();
}
}
1.7 多線程原理
分析如下代碼:
自定義線程類:
public class MyThread extends Thread{
/*
* 利用繼承中的特點
* 將線程名稱傳遞 進行設置
*/
public MyThread(String name){
super(name);
}
/*
* 重寫run方法
* 定義線程要執行的代碼
*/
public void run(){
for (int i = 0; i < 20; i++) {
//getName()方法 來自父親
System.out.println(getName()+i);
}
}
}
測試類:
public class Demo {
public static void main(String[] args) {
System.out.println("這里是main線程");
MyThread mt = new MyThread("小強");
mt.start();//開啟了一個新的線程
for (int i = 0; i < 20; i++) {
System.out.println("旺財:"+i);
}
}
}
下面畫個多線程執行時序圖來體現一下多線程程序的執行流程。
流程圖:
[圖片上傳失敗...(image-9dfe1a-1580720568911)]
程序啟動運行 main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建。隨著調用mt的對象的start方法,另外一個新的線程也啟動了,這樣,整個應用就在多線程下運行。通過這張圖我們可以很清晰的看到多線程的執行流程,那么為什么可以完成并發執行呢?我們再來講一講原理。
多線程執行時,到底在內存中是如何運行的呢?以上個程序為例,進行圖解說明:多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。
[圖片上傳失敗...(image-75c3ee-1580720568911)]
當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那么進程就結束了。
2 線程安全
2.1 線程安全
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,演示線程的安全問題:
電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個(本場電影只能賣100張票)。
我們來模擬電影院的售票窗口,實現多個窗口同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個窗口一起賣這100張票)
需要窗口,采用線程對象來模擬;需要票,Runnable接口子類來模擬
模擬票:
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個窗口賣票的操作
//窗口 永遠開啟
while (true) {
if (ticket > 0) {//有票 可以賣
//出票操作
//使用sleep模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//獲取當前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
}
測試類:
public class Demo {
public static void main(String[] args) {
//創建線程任務對象
Ticket ticket = new Ticket();
//創建三個窗口對象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同時賣票
t1.start();
t2.start();
t3.start();
}
}
運行的異常結果:
窗口3正在賣:100
窗口2正在賣:-1
窗口1正在賣:0
窗口2正在賣:100
發現程序出現了兩個問題:
- 相同的票數,比如100這張票被賣了兩回。
- 不存在的票,比如0票與-1票,是不存在的。
這種問題,幾個窗口(線程)票數不同步了,這種問題稱為線程不安全。
線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程并發訪問一個資源的安全性問題:也就是解決重復票與不存在票問題,Java中提供了同步機制(synchronized)來解決。
為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。
那么怎么去使用呢?有三種方式完成同步操作:
同步代碼塊。
同步方法。
鎖機制。
3.2 同步代碼塊
同步代碼塊 : synchronized 關鍵字可以用于方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:
synchronized(同步鎖){
需要同步操作的代碼
}
同步鎖:
對象的同步鎖只是一個概念,可以想象為在對象上標記了一個鎖.
- 鎖對象 可以是任意類型。
- 多個線程對象 要使用同一把鎖。
注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等著(BLOCKED)。
使用同步代碼塊解決代碼:
public class Ticket implements Runnable {
private int ticket = 100;
//創建一個鎖對象
Object lock = new Object();
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個窗口賣票的操作
//窗口 永遠開啟
while (true) {
synchronized (lock) {
if (ticket > 0) {//有票 可以賣
//出票操作
//使用sleep模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//獲取當前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
}
}
當使用了同步代碼塊后,上述的線程的安全問題,解決了。
3.3 同步方法
同步方法 :使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等著。
格式:
public synchronized void method(){
可能會產生線程安全問題的代碼
}
同步鎖是誰?
對于非static方法,同步鎖就是this。
對于static方法,我們使用當前方法所在類的字節碼對象(類名.class)。
使用同步方法代碼如下:
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個窗口賣票的操作
//窗口 永遠開啟
while(true){
sellTicket();
}
}
/*
* 鎖對象是誰調用這個方法就是誰
* 隱含鎖對象就是this
* 相當于synchronized (this){}
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以賣
//出票操作
//使用sleep模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//獲取當前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在賣:"+ticket--);
}
}
}
3.4 Lock 鎖
java.util.concurrent.locks.Lock
機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
public void lock() :加同步鎖。
public void unlock() :釋放同步鎖。
使用如下
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個窗口賣票的操作
//窗口 永遠開啟
while(true){
lock.lock();
if(ticket>0){//有票 可以賣
//出票操作
//使用sleep模擬一下出票時間
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//獲取當前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在賣:"+ticket--);
}
lock.unlock();
// //另一種寫法,將unlock放在finally里面
// lock.lock();
// if(ticket>0){//有票 可以賣
// //出票操作
// //使用sleep模擬一下出票時間
// try {
// Thread.sleep(10);
// //獲取當前線程對象的名字
// String name = Thread.currentThread().getName();
// System.out.println(name+"正在賣:"+ticket--);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } finally {
// lock.unlock();
// }
// }
// }
}
}
}
3 線程狀態
3.1 線程狀態概述
當線程被創建并啟動以后,它既不是一啟動就進入了執行狀態,也不是一直處于執行狀態。在線程的生命周期中,有幾種狀態呢?在API中 java.lang.Thread.State
這個枚舉中給出了六種線程狀態:
這里先列出各個線程狀態發生的條件,下面將會對每種狀態進行詳細解析
線程狀態 | 導致狀態發生條件 |
---|---|
NEW(新建) | 線程剛被創建,但是并未啟動。還沒調用start方法。 |
Runnable(可運行) | 線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決于操作系統處理器。 |
Blocked(鎖阻塞) | 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。 |
Waiting(無限等待) | 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態后是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 |
Timed Waiting(計時等待) | 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被終止) | 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。 |
我們不需要去研究這幾種狀態的實現原理,我們只需知道在做線程操作中存在這樣的狀態。那我們怎么去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間的轉換問題。
3.2 Timed Waiting (計時等待)
Timed Waiting在API中的描述為:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處于這一狀態。單獨的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪里呢?
在我們寫賣票的案例中,為了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。
其實當我們調用了sleep方法之后,當前執行的線程就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等待),那么我們通過一個案例加深對該狀態的一個理解。
實現一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字出一個字符串
代碼:
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("‐‐‐‐‐‐‐" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 線程睡眠1秒!\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
通過案例可以發現, sleep方法的使用還是很簡單的。我們需要記住下面幾點:
- 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協
作關系。 - 為了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程
中會睡眠 - sleep與鎖無關,線程睡眠到期自動蘇醒,并返回到Runnable(可運行)狀態。
小提示:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期后就
開始立刻執行。
Timed Waiting 線程狀態圖:
[圖片上傳失敗...(image-4896a9-1580720568911)]
3.3 BLOCKED (鎖阻塞)
Blocked 狀態在API中的介紹為:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處于這一狀態。
我們已經學完同步機制,那么這個狀態是非常好理解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那么線程B就進入到Blocked鎖阻塞狀態。
這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態,而這部分內容作為擴充知識點帶領大家了解一下。
Blocked 線程狀態圖
[圖片上傳失敗...(image-84a5a7-1580720568911)]
3.4 Waiting (無限等待)
Wating狀態在API中介紹為:一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處于這一狀態。
那么我們之前遇到過這種狀態嗎?答案是并沒有,但并不妨礙我們進行一個簡單深入的了解。我們通過一段代碼來學習一下:
public class WaitingTest {
public static Object obj = new Object();
public static void main(String[] args) {
// 演示waiting
new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (obj){
try {
System.out.println( Thread.currentThread().getName() +"=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象");
obj.wait(); //無限等待
//obj.wait(5000); //計時等待, 5秒 時間到,自動醒來
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了");
}
}
}
},"等待線程").start();
new Thread(new Runnable() {
@Override
public void run() {
// while (true){ //每隔3秒 喚醒一次
try {
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒鐘");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 獲取到鎖對象,調用notify方法,釋放鎖對象");
obj.notify();
}
}
// }
},"喚醒線程").start();
}
}
通過上述案例我們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的Object.notify()方法 或 Object.notifyAll()方法。
其實waiting狀態并不是一個線程的操作,它體現的是多個線程間的通信,可以理解為多個線程之間的協作關系,多個線程會爭取鎖,同時相互之間又存在協作關系。就好比在公司里你和你的同事們,你們可能存在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。
當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那么A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那么就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那么A線程喚醒后就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那么就進入到Blocked(鎖阻塞狀態)。
Waiting 線程狀態圖
[圖片上傳失敗...(image-16ee09-1580720568911)]
3.5 補充知識點
到此為止我們已經對線程狀態有了基本的認識,想要有更多的了解,詳情可以見下圖:
[圖片上傳失敗...(image-a962c0-1580720568912)]
tips:
我們在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀態聯系還是很緊密的,比如Waiting(無限等待) 狀態中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。這種帶參的方法,其實是一種倒計時操作,相當于我們生活中的小鬧鐘,我們設定好時間,到時通知,可是如果提前得到(喚醒)通知,那么設定好時間在通知也就顯得多此一舉了,那么這種設計方案其實是一舉兩得。如果沒有得到(喚醒)通知,那么線程就處于Timed Waiting狀態,直到倒計時完畢自動醒來;如果在倒計時期間得到(喚醒)通知,那么線程從Timed Waiting狀態立刻喚醒。
4 等待喚醒機制
4.1 線程間通信
概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。
比如:線程A用來生成包子的,線程B用來吃包子的,包子可以理解為同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那么線程A與線程B之間就存在線程通信問題。
[圖片上傳失敗...(image-4799c3-1580720568912)]
為什么要處理線程間通信:
多個線程并發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那么多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。
如何保證線程間通信有效利用資源:
多個線程在處理同一個資源,并且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
4.2 等待喚醒機制
什么是等待喚醒機制
這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這并不是故事的全部,線程間也會有協作機制。就好比在公司里你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。
就是在一個線程進行了規定操作后,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過后 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。
wait/notify 就是線程間的一種協作機制。
等待喚醒中的方法
等待喚醒機制就是用于解決線程間通信的問題的,使用到的3個方法的含義如下:
- wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等著別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
- notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置后,等候就餐最久的顧客最先入座。
- notifyAll:則釋放所通知對象的 wait set 上的全部線程。
注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功后才能在當初調用 wait 方法之后的地方恢復執行。
總結如下:
- 如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
- 否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態
調用wait和notify方法需要注意的細節
- wait方法與notify方法必須要由同一個鎖對象調用。因為:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程。
- wait方法與notify方法是屬于Object類的方法的。因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
- wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因為:必須要通過鎖對象調用這2個方法。
4.3 生產者與消費者問題
等待喚醒機制其實就是經典的“生產者與消費者”的問題。
就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:
包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態為false),吃貨線程等待,包子鋪線程生產包子(即包子狀態為true),并通知吃貨線程(解除吃貨的等待狀態),因為已經有包子了,那么包子鋪線程進入等待狀態。接下來,吃貨線程能否進一步執行則取決于鎖的獲取情況。如果吃貨獲取到鎖,那么就執行吃包子動作,包子吃完(包子狀態為false),并通知包子鋪線程(解除包子鋪的等待狀態),吃貨線程進入等待。包子鋪線程能否進一步執行則取決于鎖的獲取情況。
代碼演示:
包子資源類:
public class BaoZi {
String pier ;
String xianer ;
boolean flag = false ;//包子資源 是否存在 包子資源狀態
}
吃貨線程類:
public class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
while(true){
synchronized (bz){
if(bz.flag == false){//沒包子
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃貨正在吃"+bz.pier+bz.xianer+"包子");
bz.flag = false;
bz.notify();
}
}
}
}
包子鋪線程類:
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
int count = 0;
//造包子
while(true){
//同步
synchronized (bz){
if(bz.flag == true){//包子資源 存在
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 沒有包子 造包子
System.out.println("包子鋪開始做包子");
if(count%2 == 0){
// 冰皮 五仁
bz.pier = "冰皮";
bz.xianer = "五仁";
}else{
// 薄皮 牛肉大蔥
bz.pier = "薄皮";
bz.xianer = "牛肉大蔥";
}
count++;
bz.flag=true;
System.out.println("包子造好了:"+bz.pier+bz.xianer);
System.out.println("吃貨來吃吧");
//喚醒等待線程 (吃貨)
bz.notify();
}
}
}
}
測試類:
public class Demo {
public static void main(String[] args) {
//等待喚醒案例
BaoZi bz = new BaoZi();
ChiHuo ch = new ChiHuo("吃貨",bz);
BaoZiPu bzp = new BaoZiPu("包子鋪",bz);
ch.start();
bzp.start();
}
}
執行效果:
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
5 線程池
5.1 線程池思想概述
[圖片上傳失敗...(image-c7bfc4-1580720568912)]
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:
如果并發的線程數量很多,并且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。
那么有沒有一種辦法使得線程可以復用,就是執行完一個任務,并不被銷毀,而是可以繼續執行其他的任務?
在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池。
5.2 線程池概念
- 線程池:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。
由于線程池中有很多操作都是與優化資源相關的,我們在這里就不多贅述。我們通過一張圖來了解線程池的工作原理:
[圖片上傳失敗...(image-bb787a-1580720568912)]
合理利用線程池能夠帶來三個好處:
- 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
5.3 線程池的使用
Java里面線程池的頂級接口是java.util.concurrent.Executor
,但是嚴格意義上講Executor
并不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService
。
要配置一個線程池是比較復雜的,尤其是對于線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors
線程工廠類里面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。
Executors類中有個創建線程池的方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads)
:返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)
獲取到了一個線程池ExecutorService 對象,那么怎么使用呢,在這里定義了一個使用線程池對象的方法如下:
-
public Future<?> submit(Runnable task)
:獲取線程池中的某一個線程對象,并執行Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。
使用線程池中線程對象的步驟:
- 創建線程池對象。
- 創建Runnable接口子類對象。(task)
- 提交Runnable接口子類對象。(take task)
- 關閉線程池(一般不做)。
Runnable實現類代碼:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一個教練");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教練來了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教練回到了游泳池");
}
}
線程池測試類:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 創建線程池對象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
// 創建Runnable實例對象
MyRunnable r = new MyRunnable();
//自己創建線程對象的方式
// Thread t = new Thread(r);
// t.start(); ---> 調用MyRunnable中的run()
// 從線程池中獲取線程對象,然后調用MyRunnable中的run()
service.submit(r);
// 再獲取個線程對象,調用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法調用結束后,程序并不終止,是因為線程池控制了線程的關閉。
// 將使用完的線程又歸還到了線程池中
// 關閉線程池
//service.shutdown();
}
}