目錄
1 執行緒與行程
2 執行緒調度
3 執行緒的兩種實作方式
4 Thread 的各種方法
5 執行緒安全
6 執行緒死鎖
7 執行緒間通信/互動
8 執行緒的六種狀態
9 執行緒的第三種實作方式
10 執行緒池
1 執行緒與行程
行程:一個記憶體中運行的應用程式,每個行程都有一個獨立的記憶體空間,
執行緒:行程中的一個執行路徑,同一行程中的執行緒共享一個記憶體空間,執行緒之間可以自由切換,并發執行,
-
一個行程最少擁有一個執行緒,
-
執行緒實際上是在行程的基礎上的進一步劃分,一個行程啟動后,里面的若干執行路徑又可以劃分成若干個路線,
-
每個執行緒都擁有自己的堆疊空間(也就是說,由同一個執行緒呼叫的方法,都會在該執行緒內部執行),執行緒之間共用一份堆記憶體,
2 執行緒調度
-
分時調度
所有執行緒輪流獲得 CPU 的使用權,平均分配每個執行緒占用 CPU 的時間,
-
搶占式調度
優先級高的執行緒先使用 CPU,如果執行緒的優先級相同,則誰拿到使用權是隨機的(執行緒隨機性),
Java 中使用的執行緒調度方式為搶占式調度,CPU 采用搶占式調度模式在多個執行緒間進行著高速切換,某個時刻CPU 的一個核只能執行一個執行緒,由于切換速度飛快,看上去就像在同時進行多個任務一樣, 所以多執行緒并不能提高程式的運行速度,但通過提高 CPU 的使用率,從而提高了程式的運行效率,
3 執行緒的兩種實作方式
- 繼承
Thread類
通過繼承Thread類定義一個執行緒類,并通過重寫run()方法,在run()方法中完成執行緒需要執行的任務,在一個現有執行緒中創建一個執行緒類并呼叫其start()方法開啟執行緒(注意不是直接呼叫run()方法,直接呼叫run()并不會啟動新執行緒,而只是在當前執行緒中呼叫了一個方法而已)
// MyThread.java
// 定義一個執行緒類
public class MyThread extends Thread {
// run() 中的程式就是執行緒要執行的任務
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("秦時明月漢時關" + i);
}
}
}
// Test.java
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("萬里長征人未還" + i);
}
}
}
運行結果:
秦時明月漢時關0
萬里長征人未還0
秦時明月漢時關1
萬里長征人未還1
秦時明月漢時關2
萬里長征人未還2
秦時明月漢時關3
萬里長征人未還3
秦時明月漢時關4
萬里長征人未還4
秦時明月漢時關5
萬里長征人未還5
秦時明月漢時關6
萬里長征人未還6
秦時明月漢時關7
秦時明月漢時關8
萬里長征人未還7
秦時明月漢時關9
萬里長征人未還8
萬里長征人未還9
- 實作
Runnable介面
通過實作Runnable介面定義一個任務類,再將該任務類物件交給Thread物件執行,從而完成執行緒的創建,
// MyRunnable.java
// 在 MyRunnable 類中重寫 run() 寫入需要執行的任務
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("但使龍城飛將在" + i);
}
}
}
// Test.java
public class Test {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("不教胡馬度陰山" + i);
}
}
}
運行結果:
但使龍城飛將在0
不教胡馬度陰山0
但使龍城飛將在1
不教胡馬度陰山1
但使龍城飛將在2
不教胡馬度陰山2
但使龍城飛將在3
不教胡馬度陰山3
但使龍城飛將在4
不教胡馬度陰山4
但使龍城飛將在5
不教胡馬度陰山5
但使龍城飛將在6
不教胡馬度陰山6
但使龍城飛將在7
不教胡馬度陰山7
但使龍城飛將在8
不教胡馬度陰山8
但使龍城飛將在9
不教胡馬度陰山9
實作Runnable的方式跟繼承Thread的方式相比具有如下優勢:
- 通過創建任務再給執行緒分配的方式實作多執行緒,更適合多個執行緒都執行相同任務的情況;
- 可以避免單繼承所帶來的麻煩(
Runnable是介面,Thread是類); - 任務與執行緒是分離的,降低了耦合性,提高了程式的健壯性
- 執行緒池技術中,接收
Runnable型別的任務,而不接收Thread型別的執行緒,
- 內部類
當然,如果只需要執行一次某個執行緒的話,通過以內部類或者匿名內部類的方式繼承Thread可以很簡單的實作,內部類的另一個好處是可以很方便的訪問外部的區域變數,
// 新執行緒
new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("秦時明月漢時關" + i);
}
}
}.start();
// 當前執行緒
for (int i = 0; i < 10; i++) {
System.out.println("萬里長征人未還" + i);
}
4 Thread 的各種方法
- 執行緒名稱
class MyRunable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()); // 獲取當前執行緒名稱
}
}
// Test.java
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()); // 主執行緒名稱為 "main"
new Thread(new MyRunable(), "壹").start(); // 設定執行緒名稱為 "貳"
Thread t = new Thread(new MyRunable());
System.out.println(t.getName()); // 默認名稱為 "Thread-0"
t.setName("貳"); // 設定執行緒名稱為 "貳"
t.start();
new Thread(new MyRunable()).start(); // 默認名稱為 "Thread-1"
new Thread(new MyRunable()).start(); // 默認名稱為 "Thread-2"
}
}
運行結果:
main
Thread-0
壹
Thread-2
Thread-1
貳
- 執行緒休眠
public class Test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
System.out.println(i);
Thread.sleep(1000); // 執行緒休眠 1s
}
}
}
運行結果:從 0 開始每隔 1s 輸出加 1
0
1
2
3
4
還有一個多載的sleep()兩參方法:
// 毫秒 + 納秒
// 納秒允許范圍 [0, 999999]
public static void sleep(long millis, int nanos) {}
- 執行緒中斷
通過呼叫執行緒的interrupt(),通知執行緒關閉,由執行緒中的任務代碼決定是否結束自己的生命,
// MyRunable.java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(1000); // 執行緒在 sleep 期間檢查是否有中斷標記,若有則拋出例外
} catch (InterruptedException e) {
System.out.println("發現中斷標記,執行緒自殺");
return; // 任務終止,執行緒從此結束
}
}
}
}
// Test.java
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyRunnable());
t.start();
// 主執行緒每隔一秒輸出一次數字,共輸出 5 次,因此主執行緒回圈先結束
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.sleep(1000);
}
t.interrupt(); // 結束回圈之后通知執行緒 t 中斷
}
}
運行結果:
Thread-0: 0
main: 0
main: 1
Thread-0: 1
Thread-0: 2
main: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
Thread-0: 5
發現中斷標記,執行緒自殺
- 守護執行緒
- 執行緒分為用戶執行緒和守護執行緒,
- 用戶執行緒:當一個行程不包含任何存活的用戶執行緒時,行程結束;
- 守護執行緒:用于守護用戶執行緒,通常被用來做日志、性能統計等作業,當一個行程中最后一個用戶執行緒結束時,所有守護執行緒自動終止,行程結束,
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
public void run(){
for (int i = 0; i < 10; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s 運行了:%ss\n",Thread.currentThread().getName(), i);
}
}
};
t.setDaemon(true); // 設定為守護執行緒
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.sleep(1000);
}
}
}
運行結果:
main: 0
Thread-0 運行了:0s
main: 1
Thread-0 運行了:1s
main: 2
Thread-0 運行了:2s
main: 3
Thread-0 運行了:3s
main: 4
Thread-0 運行了:4s
5 執行緒安全
- 執行緒同步問題
// Ticket.java
public class Ticket implements Runnable{
private int count = 5; // 剩余票數
@Override
public void run() {
while (count > 0) {
System.out.println("出票中...");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票為:" + count);
}
}
}
// Test.java
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
運行結果:
出票中...
出票中...
出票中...
出票成功,余票為:4
出票中...
出票成功,余票為:3
出票中...
出票成功,余票為:2
出票中...
出票成功,余票為:-1
出票成功,余票為:0
出票成功,余票為:1
上面例子中出現了最后余票為 -1 的情況,原因是在最后階段余票為 1 時,同時有不止一個執行緒進入了出票程式中,造成了不止一次的出票操作,因此上面代碼的操作流程是執行緒不安全的,
-
執行緒同步
synchronized同步代碼塊
synchronized同步代碼塊使用一個物件 o 作為鎖,表示當前執行緒獨占該物件,這個物件又叫做同步物件,任何物件都可以作為同步物件,Object o = new Object(); synchronized (lock){ //只有占有了 lock 后才可以執行此塊的代碼 }如上面代碼所示為使用 synchronized 語法的格式,只有執行緒占用了鎖,它才能執行大括號部分代碼塊中的程式,執行結束又會釋放對鎖的占用,鎖被占用期間,其它試圖占用它的執行緒就會等待而不會進入代碼塊中執行,直到鎖被釋放,等待的執行緒就會爭搶該鎖,搶到占用的執行緒又進入代碼塊中執行程式,其它執行緒繼續等待,
修改上面的出票任務為同步代碼塊的方式如下:
// Ticke.java public class Ticket implements Runnable{ private int count = 5; // 剩余票數 final Object o = new Object(); @Override public void run() { while (true) { synchronized (o) { if (count > 0) { System.out.println(Thread.currentThread().getName() + "出票中..."); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println("出票成功,余票為:" + count); } else { break; } // 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } } // Test.java public class Test { public static void main(String[] args) throws InterruptedException { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } }運行結果:
Thread-0出票中... 出票成功,余票為:4 Thread-2出票中... 出票成功,余票為:3 Thread-2出票中... 出票成功,余票為:2 Thread-2出票中... 出票成功,余票為:1 Thread-1出票中... 出票成功,余票為:0可以看到出票正常,個執行緒間沒有交叉出票的情況,當然,整個程式的執行時間也變長了,和單執行緒運行時間差不多,
注意:必須使用同一個物件作為鎖,才能達到同步的效果,如果各個執行緒占用不同的物件作為鎖,實際上是沒有作用的,它們還是在一起執行同一段代碼塊,沒有實作等待的效果,
synchronized同步方法
這種方式將需要同步的代碼塊提取為一個方法,并使用
synchronized修飾,這種方式的鎖(同步物件)被隱式的指定為方法所在物件,即this物件,如果同步方法被static修飾,那么此時的同步物件為方法所在類的位元組碼檔案物件,即類名.class,修改上面的出票任務為同步方法的方式如下:
// Ticket.java public class Ticket implements Runnable{ private int count = 5; // 剩余票數 @Override public void run() { while (true) { if (!sale()) break; // 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } private synchronized boolean sale() { if (count > 0) { System.out.println(Thread.currentThread().getName() + "出票中..."); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println("出票成功,余票為:" + count); return true; } return false; } } // Test.java public class Test { public static void main(String[] args) throws InterruptedException { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } }運行結果:
Thread-0出票中... 出票成功,余票為:4 Thread-2出票中... 出票成功,余票為:3 Thread-1出票中... 出票成功,余票為:2 Thread-2出票中... 出票成功,余票為:1 Thread-0出票中... 出票成功,余票為:0
不論是同步代碼塊或者時同步方法,只要使用了同一個鎖,當鎖被某個執行緒占用時,任何使用該鎖的代碼塊或者方法都會被同步化,即其它執行緒不能執行其中的任何代碼塊或者方法,
- `Lock`顯式鎖
顯式鎖的方式通過直接定義一個 Java 中的`Lock`物件來實作同步效果,通過呼叫該物件的`lock()`方法上鎖,與`synchronized`語法自動釋放鎖不同,`Lock`物件必須通過顯式地呼叫`unlock()`方法來釋放鎖,所以,把`unlock()`放在`finally`中可以確保釋放的執行,
修改上面的出票任務為顯式鎖的方式如下:
```java
// Ticket.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int count = 5; // 剩余票數
private Lock lk = new ReentrantLock();
@Override
public void run() {
while (true) {
lk.lock();
try {
if (count <= 0) break;
System.out.println(Thread.currentThread().getName() + "出票中...");
Thread.sleep(500);
count--;
System.out.println("出票成功,余票為:" + count);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lk.unlock();
}
// 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Test.java
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
運行結果:
Thread-1出票中...
出票成功,余票為:4
Thread-0出票中...
出票成功,余票為:3
Thread-2出票中...
出票成功,余票為:2
Thread-1出票中...
出票成功,余票為:1
Thread-0出票中...
出票成功,余票為:0
Java 中的Lock是一個介面,所以需要使用它的實作類ReentrantLock創建一個Lock物件,同樣的,為了達到同步的效果,即執行緒安全的目的,必須要讓各個執行緒使用同一個Lock物件才行,
- 公平鎖
上述的三種執行緒同步的實作中,使用的都不是公平鎖,前面說過 Java 中采用搶占式調度模式切換執行緒,就是說,當某個鎖被釋放時,并不一定是最先前來請求占用的執行緒占用到該鎖,而是隨機的,所以是“不公平”的,
可以在顯式鎖的方式中實作公平鎖,即讓執行緒排隊獲取鎖的占用權,只需要在創建ReentrantLock物件時,傳入為boolean型別引數傳入true就行,
Lock lk = new ReentrantLock(true);
6 執行緒死鎖
執行緒死鎖的現象:兩個執行緒占有了對方需要的鎖,導致了兩邊都在等待對方釋放鎖,使得程式永遠處于僵持狀態,或者多個執行緒之間占用了后面執行緒需要的鎖,形成了死鎖環,下面通過一個應聘者和招聘者之間的例子展示兩個執行緒之間的死鎖現象,
// Candidate.java
public class Candidate {
public synchronized void sayTo(Recruiter recruiter) {
System.out.println("應聘者:你給我作業,我就有作業經驗了!");
// 停一段時間,給另一個執行緒有足夠的時間占用 recruiter 鎖
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等待應聘者回應
recruiter.response();
}
public synchronized void response() {
System.out.println("好我有作業經驗了!");
}
}
// Recruiter.java
public class Recruiter {
public synchronized void sayTo(Candidate candidate) {
System.out.println("面試官:你有作業經驗,我就給你作業!");
// 停一段時間,另一個執行緒有足夠的時間占用 candidate 鎖
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等待應聘者回應
candidate.response();
}
public synchronized void response() {
System.out.println("好我給你作業!");
}
}
// DeadLock.java 對話僵局
public class DeadLock {
public static void main(String[] args) {
Candidate candidate = new Candidate();
Recruiter recruiter = new Recruiter();
new Thread(new Runnable() {
@Override
public void run() {
candidate.sayTo(recruiter);
}
}).start();
recruiter.sayTo(candidate);
}
}
程式會進入死鎖,物件recruiter在等待物件candidate釋放作為鎖的自己,反過來candidate也在等待recruiter釋放作為鎖的自己,程式卡住了,永遠不會結束,局面十分焦灼,,,
在程式中避免死鎖現象的最直接有效的辦法就是,在會產生鎖的代碼塊或方法中不要呼叫另外一個會產生鎖的代碼塊或方法,
7 執行緒間通信/互動
同步執行緒之間通過呼叫鎖物件的wait()/notify()/notifyAll()方法相互通知,某個執行緒通過呼叫鎖物件.wait()讓自己進入等待狀態,之后該執行緒不再暫停運行,直到另外某個執行緒呼叫鎖物件.notify(),前面暫停的執行緒才可能重新回到運行狀態,只是可能,因為notify()只會讓處于等待狀態的執行緒中的一個重新運行,或者當某個執行緒運行中呼叫鎖物件.notifyAll(),這會讓所有處于等待狀態中的執行緒都恢復運行,下面是一個炒菜與上菜的簡單例子:
// Food.java 食物
public class Food {
private String name;
private String taste;
// 做菜
public synchronized void setNameAndTaste(String name, String taste) {
System.out.println("開始做菜...");
this.name = name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
System.out.println("菜做好了,");
}
// 上菜
public synchronized void get() {
System.out.println("上菜: ");
System.out.println(this.name + "-" + this.taste);
}
}
// Cook.java 廚師
public class Cook implements Runnable{
private final Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 2 == 0) {
f.setNameAndTaste("西紅柿炒番茄", "麻辣味");
} else {
f.setNameAndTaste("馬鈴薯燒土豆", "芥末味");
}
}
}
}
// Waiter.java 服務員
public class Waiter implements Runnable{
private final Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
f.get();
}
}
}
// 上菜服務
public class Serving {
public static void main(String[] args) {
Food f = new Food();
new Thread(new Cook(f)).start();
new Thread(new Waiter(f)).start();
}
}
運行結果:
開始做菜...
菜做好了,
上菜:
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
開始做菜...
菜做好了,
開始做菜...
菜做好了,
上面程式中,即使Food中的方法已經同步化了,使得做菜setNameAndTaste()和上菜get()的程序你不會相互干擾,但上菜服務的流程仍然是不合理的,waiter再上完一道菜后,沒等cook 呼叫setNameAndTaste()做下一道菜,就開始繼續呼叫get()上菜了,同樣可能的是,cook做完一道菜還沒等waiter呼叫get()上菜,就繼續呼叫setNameAndTaste()換做下一道菜了,這個問題稱為生產者與消費者問題,這里cook作為生產者,waiter作為消費者,
合理的流程應該是生產者在產生資料時,消費者應該暫停執行使用資料的操作,反過來,消費者在使用資料時,生產者應該暫停更新資料的才做,上面例子的問題可以通過執行緒間通信的方式解決,需要做修改的是Food類中的setNameAndTaste()和get():
public class Food {
private String name;
private String taste;
public synchronized void setNameAndTaste(String name, String taste) {
System.out.println("開始做菜...");
this.name = name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
System.out.println("菜做好了,");
this.notifyAll(); // 通知其它執行緒恢復運行
try {
this.wait(210); // 進入等待狀態 210ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void get() {
System.out.println("上菜: ");
System.out.println(this.name + "-" + this.taste);
this.notifyAll(); // 通知其它執行緒恢復運行
try {
this.wait(210); // 進入等待狀態 210ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這樣的Serving.java運行結果為:
開始做菜...
菜做好了,
上菜:
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜:
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
上菜:
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜:
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
上菜:
西紅柿炒番茄-麻辣味
8 執行緒的六種狀態
執行緒的六種狀態以列舉形式存在于Thread類中,
public enum State {
NEW, // 已創建未運行的狀態
RUNNABLE, // 運行狀態
BLOCKED, // 阻塞狀態,等待占用鎖物件從而進入到同步代碼塊或者同步方法中
WAITING, // 等待狀態,
TIMED_WAITING, // 定時等待狀態,執行緒暫停運行一段時間
TERMINATED; // 執行緒生命結束
}
這幾種狀態再執行緒的生命周期中的轉換關系如下圖:
9 執行緒的第三種實作方式
Java 中除了上面使用Thread和Runnable的兩種方式,還可以使用Callable介面實作執行緒,與前兩種方式不同,Callable可以實作有回傳值的執行緒,而前兩種沒有回傳值,
使用Callable實作執行緒的步驟如下:
- 實作
Callable介面,實作其中的call()方法; - 創建
FutureTask物件,并傳入一個前面實作Callable介面的類的物件; - 通過
Thread創建執行緒,傳入第二步中的FutureTask物件作為執行緒的執行任務,啟動執行緒,
獲取執行緒回傳值的方式是呼叫FutureTask物件的get()方法,此方法會阻塞主行程執行,一直等到FutureTask物件所在執行緒結束回傳,具體實作如下例:
// MyCallable.java
import java.util.concurrent.Callable;
// 1. 實作 Callable 介面
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ": " + i);
}
return 100;
}
}
// Test.java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args)
throws ExecutionException, InterruptedException {
// 2. 創建 FutureTask 物件,并傳入一個實作 Callable 介面的類的物件;
Callable<Integer> call = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(call);
// 3. 通過 Thread 創建執行緒,傳入 task,啟動執行緒,
new Thread(task).start();
Integer num = task.get(); // 主執行緒會等待執行 task 的執行緒執行結束并接收一個回傳值,然后再繼續往下執行
System.out.println("task 回傳結果:" + num);
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
運行結果:
Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
task 回傳結果:100
main: 0
main: 1
main: 2
main: 3
main: 4
我們也可以在主執行緒中不用一直等待task所在執行緒結束,而是在主執行緒執行程序中某個時機呼叫task.isDone()來查看task所在執行緒是否結束,檢查到已經結束時,在取得其回傳值,具體來說,修改上面Test.java如下:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> call = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(call);
System.out.println("task 回傳結果:" + num);
for (int i = 0; i < 10; i++) {
if (task.isDone()) {
int num = task.get();
System.out.println("task 回傳結果:" + num);
break;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
運行結果:
Thread-0: 0
main: 0
Thread-0: 1
main: 1
main: 2
Thread-0: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
main: 5
task 回傳結果:100
還可以通過呼叫task.cancel(true)來主動取消task所在執行緒的執行,如果取消成功,該方法會回傳true,這表示該執行緒在自然結束之前被取消了;如果取消失敗,則回傳false,這就表示該執行緒在執行取消操作之前已經結束了,
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> call = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(call);
new Thread(task).start();
for (int i = 0; i < 3; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ": " + i);
}
if (task.cancel(ture)) {
System.out.println("取消成功!");
}
}
}
運行結果:
main: 0
Thread-0: 0
main: 1
Thread-0: 1
main: 2
Thread-0: 2
取消成功!
10 執行緒池
如果為了頻繁地執行很多任務,且這些任務的執行時間很短,我們為此頻繁地創建了很多執行緒,因為創建執行緒和銷毀執行緒需要時間,所以大量的時間都消耗在了執行緒的創建和銷毀上,這樣就導致了系統的效率大大降低, 執行緒池就是為了解決這個問題的,執行緒池是一個容納多個執行緒的容器,其中的執行緒可以反復使用,省去了頻繁創建執行緒物件的操作,從而可以省下很多的時間和資源,
執行緒池的好處:
- 降低資源消耗
- 提高回應速度
- 提高執行緒的可管理性
Java 中提供了四種執行緒池,分別是快取執行緒池、定長執行緒池、單執行緒執行緒池和周期性任務定長執行緒池,
-
快取執行緒池
快取執行緒池的執行緒陣列長度無限制,可以變化,其執行流程是:
- 判斷執行緒池是否存在空閑執行緒
- 存在則使用
- 不存在,則創建執行緒 并放入執行緒池, 然后使用
// 使用快取線城市
ExecutorService service = Executors.newCachedThreadPool();
// 指揮執行緒池執行新的任務
service.execute(() -> System.out.println(Thread.currentThread().getName() + " haha"));
service.execute(() -> System.out.println(Thread.currentThread().getName() + " keke"));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(() -> System.out.println(Thread.currentThread().getName() + " didi"));
運行結果(可以看到第三個任務在前面的第二個執行緒中執行,因為前面的執行緒已經空閑下來了):
pool-1-thread-2 keke
pool-1-thread-1 haha
pool-1-thread-2 didi
-
定長執行緒池
定長執行緒池的執行緒陣列的長度是指定不變的,其執行流程如下:
- 判斷執行緒池是否存在空閑執行緒
- 存在則使用
- 不存在空閑執行緒,且執行緒池未滿的情況下,則創建執行緒并放入執行緒池,,然后使用
- 不存在空閑執行緒,且執行緒池已滿的情況下,則等待執行緒池存在空閑執行緒
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " hahaha");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " hahaha");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(() -> System.out.println(Thread.currentThread().getName() + "hahaha"));
運行結果(可以看到執行緒池不會第三個任務創建新的執行緒,而是會等待前兩個執行緒空閑下來再執行第三個任務):
pool-1-thread-1 hahaha
pool-1-thread-2 xixixi
pool-1-thread-1 yuyuyu
-
單執行緒執行緒池
單執行緒執行緒池相當于將定長執行緒池的執行緒陣列長度指定為 1,所以執行流程和前面一樣,只是始終只有一個執行緒可供使用,它可以用來操作需要排隊執行的任務,
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " hahaha");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " xixixi");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " bobobo");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
運行結果(可以看到從頭到尾只有一個執行緒執行任務):
pool-1-thread-1 hahaha
pool-1-thread-1 xixixi
pool-1-thread-1 bobobo
-
周期性任務定長執行緒池
周期性任務定長執行緒池的執行流程和定長執行緒池差不多,但它可以在同一個執行緒中周期性地執行同一個任務,
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
// 指定 3 個時間單位后執行任務,時間單位為秒
service.schedule(() -> System.out.println(Thread.currentThread().getName() + "hehe"), 3, TimeUnit.SECONDS);
// 指定 2 個時間單位后執行任務,每個 1 個時間單位執行一次同樣的任務,時間單位為秒
service.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "wuwu"), 2, 1, TimeUnit.SECONDS);
運行結果(pool-1-thread-1 在 2s 時啟動,并每隔 1 秒列印 wuwu,pool-1-thread-2 在 3s 時啟動并列印一次 hehe 后結束):
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-2 hehe
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
... ...
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297000.html
標籤:Java
