水滴石穿--多執行緒入門
前記:
鄙人大三資訊管理與資訊系統專業,寫這一個系列的目的就是為了好好磨磨自己的秉性,再好好從基礎一步一步走一遍,順便總結總結自己寫程式碼時才過的坑,也為下一次機會做準備。這一學年我通過傳智播客視訊課的形式學習了JAVA EE,又在假期好好看了一些計算機方向的書籍,自以為有所收穫(其實自己是過於自信)。一次學校導員發的一個百度javaee實習資訊,我就自不量力的附上了自己的簡歷去試試,人家直接電話面試一些基礎的東西都是自己做過的東西,結果忘了。答的不好所以直接涼涼,那時候起我就開始自我反省,真的是自己的太過浮躁,學習知識浮於表面不深究,儘管自己找了好多不錯的資源,但是沒有真正溶於自己的知識體系中,所以我想用剩下幾個月的時間重新走走以前學過的知識,為明年春招實習鞏固基礎。
活不多說直奔主題,多執行緒面試的重點、難點,也是java的基本功所以我會在這一塊多下點功夫。
執行緒與程序區別:
每個正在系統上執行的程式都是一個程序。每個程序包含一到多個執行緒。執行緒是一組指令的集合,或者是程式的特殊段,它可以在程式裡獨立執行。也可以把它理解為程式碼執行的上下文。所以執行緒基本上是輕量級的程序,它負責在單個程式裡執行多工。通常由作業系統負責多個執行緒的排程和執行。
使用執行緒可以把佔據時間長的程式中的任務放到後臺去處理,程式的執行速度可能加快,在一些等待的任務實現上如使用者輸入、檔案讀寫和網路收發資料等,執行緒就比較有用了。在這種情況下可以釋放一些珍貴的資源如記憶體佔用等等。
如果有大量的執行緒,會影響效能,因為作業系統需要在它們之間切換,更多的執行緒需要更多的記憶體空間,執行緒的中止需要考慮其對程式執行的影響。通常塊模型資料是在多個執行緒間共享的,需要防止執行緒死鎖情況的發生。
總結:程序是所有執行緒的集合,每一個執行緒是程序中的一條執行路徑。
為什麼要使用多執行緒?
答:主要能體現到多執行緒提高程式效率。
舉例: 迅雷多執行緒下載、資料庫連線池、分批發送簡訊等。
多執行緒建立方式(3種)
1.繼承Thread類 重寫run方法
/** * @classDesc:執行緒入門,執行緒的建立通過繼承Thread實現,重寫run方法 * @author: hj * @date:2018年12月11日 下午10:26:03 */ public class createThread extends Thread { /* run方法中編寫多執行緒要執行的程式碼 */ @Override public void run() { System.out.println("run 方法被重寫了,正在被呼叫。。。"); } public static void main(String[] args) { System.out.println("-----多執行緒建立開始-----"); // 1.建立一個執行緒 createThread createThread = new createThread(); System.out.println("-----多執行緒建立啟動-----"); // 2.開始執行執行緒 注意 開啟執行緒不是呼叫run方法,而是start方法 createThread.start(); System.out.println("-----多執行緒建立結束-----"); } }
允許結果:
-----多執行緒建立開始-----
-----多執行緒建立啟動-----
-----多執行緒建立結束-----
run 方法被重寫了,正在被呼叫。。。
2.實現Runnable介面,重寫run方法
/**
* @classDesc:執行緒的建立(實現Runnable介面,重寫run方法)
* @author: hj
* @date:2018年12月11日 下午10:33:21
*/
public class CreateRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
public static void main(String[] args) {
System.out.println("-----多執行緒建立開始-----");
// 1.建立一個執行緒
CreateRunable createThread = new CreateRunable();
// 2.開始執行執行緒 注意 開啟執行緒不是呼叫run方法,而是start方法
System.out.println("-----多執行緒建立啟動-----");
Thread thread = new Thread(createThread);
thread.start();
System.out.println("-----多執行緒建立結束-----");
}
}
允許結果:-----多執行緒建立開始-----
-----多執行緒建立啟動-----
-----多執行緒建立結束-----
i:0
i:1
i:2
i:3
i:4
i:5
i:6
i:7
i:8
i:9
3.實現Callable<T>介面,重寫T call()方法
/**
* @classDesc:通過實現Callable<T>介面建立執行緒
* @author: hj
* @date:2018年12月11日 下午10:42:17
*/
public class CreateCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("this is thread B");
return "threadB";
}
public static void main(String[] args) {
CreateCallable createC = new CreateCallable();
FutureTask<String> futureTask = new FutureTask<>(createC);
new Thread(futureTask).start();
System.out.println("this is main thread!! begin...");
try {
System.out.println("result is: " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("this is main thread!! end");
}
}
允許結果:
this is main thread!! begin...
this is thread B
result is: threadB
this is main thread!! end
ps:啟動執行緒是呼叫run方法還是start方法??推薦start,不過經過測試run方法也可以啟動執行緒。下去查了一下start方法會調run方法。 參考文章可以看看這篇文章,講的簡單易懂。
ps:使用繼承Thread類還是使用實現Runnable介面好?
使用實現實現Runnable介面好,原因實現了介面還可以繼續繼承,繼承了類不能再繼承。如果需要返回執行緒執行的結果那就選擇實現callable介面。
同步與非同步的概念
扒一張圖說明一下:(同步:程式碼按照順序執行)(非同步:程式碼重新開一條執行路徑)
守護執行緒
Java中有兩種執行緒,一種是使用者執行緒,另一種是守護執行緒。
使用者執行緒是指使用者自定義建立的執行緒,主執行緒停止,使用者執行緒不會停止
守護執行緒當程序不存在或主執行緒停止,守護執行緒也會被停止。
使用setDaemon(true)方法設定為守護執行緒。
/**
* @classDesc:守護執行緒的演示,如果是守護執行緒,在主執行緒結束時就會自動關閉,<br>
*否則程式會一直執行,直到子程序結束
* @author: hj
* @date:2018年12月11日 下午11:06:40
*/
public class DaemonThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("我是子執行緒...");
}
}
});
/* 注意此處,嘗試開啟註釋和關閉註釋執行,體會守護程序的特性 */
// thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (Exception e) {
}
System.out.println("我是主執行緒");
}
System.out.println("主執行緒執行完畢!");
}
}
tips:記一個踩過的坑,在一個實習中做一個通訊的任務,關閉了操作介面,但是通訊執行緒還是沒有關閉,原因就是沒有設定成守護執行緒。
thread常用的API
常用執行緒api方法 |
|
start() |
啟動執行緒 |
currentThread() |
獲取當前執行緒物件 |
getID() |
獲取當前執行緒ID Thread-編號 該編號從0開始 |
getName() |
獲取當前執行緒名稱 |
sleep(long mill) |
休眠執行緒 |
Stop() |
停止執行緒, |
常用執行緒建構函式 |
|
Thread() |
分配一個新的 Thread 物件 |
Thread(String name) |
分配一個新的 Thread物件,具有指定的 name正如其名。 |
Thread(Runable r) |
分配一個新的 Thread物件 |
Thread(Runable r, String name) |
分配一個新的 Thread物件 |
多執行緒執行狀態
執行緒從建立、執行到結束總是處於下面五個狀態之一:新建狀態、就緒狀態、執行狀態、阻塞狀態及死亡狀態。
新建狀態
當用new操作符建立一個執行緒時, 例如new Thread(r),執行緒還沒有開始執行,此時執行緒處在新建狀態。 當一個執行緒處於新生狀態時,程式還沒有開始執行執行緒中的程式碼
就緒狀態
一個新建立的執行緒並不自動開始執行,要執行執行緒,必須呼叫執行緒的start()方法。當執行緒物件呼叫start()方法即啟動了執行緒,start()方法建立執行緒執行的系統資源,並排程執行緒執行run()方法。當start()方法返回後,執行緒就處於就緒狀態。
處於就緒狀態的執行緒並不一定立即執行run()方法,執行緒還必須同其他執行緒競爭CPU時間,只有獲得CPU時間才可以執行執行緒。因為在單CPU的計算機系統中,不可能同時執行多個執行緒,一個時刻僅有一個執行緒處於執行狀態。因此此時可能有多個執行緒處於就緒狀態。對多個處於就緒狀態的執行緒是由Java執行時系統的執行緒排程程式(thread scheduler)來排程的。
執行狀態
當執行緒獲得CPU時間後,它才進入執行狀態,真正開始執行run()方法.
阻塞狀態
執行緒執行過程中,可能由於各種原因進入阻塞狀態:
1>執行緒通過呼叫sleep方法進入睡眠狀態;
2>執行緒呼叫一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的呼叫者;
3>執行緒試圖得到一個鎖,而該鎖正被其他執行緒持有;
4>執行緒在等待某個觸發條件;
死亡狀態
有兩個原因會導致執行緒死亡:
1) run方法正常退出而自然死亡,
2) 一個未捕獲的異常終止了run方法而使執行緒猝死。
為了確定執行緒在當前是否存活著(就是要麼是可執行的,要麼是被阻塞了),需要使用isAlive方法。如果是可執行或被阻塞,這個方法返回true; 如果執行緒仍舊是new狀態且不是可執行的, 或者執行緒死亡了,則返回false.
join()方法作用
當在主執行緒當中執行到t1.join()方法時,就認為主執行緒應該把執行權讓給t1
/**
* @classDesc:join用法
* @author: hj
* @date:2018年12月11日 下午11:56:51
*/
public class Testjoin {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "i:" + i);
}
}
});
t1.start();
// 當在主執行緒當中執行到t1.join()方法時,就認為主執行緒應該把執行權讓給t1
try {
t1.join();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println("main" + "i:" + i);
}
}
}
優先順序
現代作業系統基本採用時分的形式排程執行的執行緒,執行緒分配得到的時間片的多少決定了執行緒使用處理器資源的多少,也對應了執行緒優先順序這個概念。在JAVA執行緒中,通過一個int priority來控制優先順序,範圍為1-10,其中10最高,預設值為5。下面是原始碼(基於1.8)中關於priority的一些量和方法。
/**
* @classDesc: 優先順序測試,設定了優先順序, 不代表每次都一定會被執行。 只是CPU排程會有限分配
* @author: hj
* @date:2018年12月12日 上午12:00:59
*/
public class PrioritytThread implements Runnable {
public static void main(String[] args) {
PrioritytThread prioritytThread = new PrioritytThread();
Thread t1 = new Thread(prioritytThread);
Thread t2 = new Thread(prioritytThread);
t1.start();
// 注意設定了優先順序, 不代表每次都一定會被執行。 只是CPU排程會有限分配
t1.setPriority(10);
t2.start();
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().toString() + "---i:" + i);
}
}
}
Yield方法
Thread.yield()方法的作用:暫停當前正在執行的執行緒,並執行其他執行緒。(可能沒有效果)
yield()讓當前正在執行的執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行的機會。因此,使用yield()的目的是讓具有相同優先順序的執行緒之間能夠適當的輪換執行。但是,實際中無法保證yield()達到讓步的目的,因為,讓步的執行緒可能被執行緒排程程式再次選中。
結論:大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。
為了方便大家總結,以後都會在後面放幾道面試題。
面試題
程序與執行緒的區別?
答:程序是所有執行緒的集合,每一個執行緒是程序中的一條執行路徑,執行緒只是一條執行路徑。
多執行緒建立方式?
答:繼承Thread或Runnable 介面或callable<T>。
是繼承Thread類好還是實現Runnable介面好?
答:Runnable介面好,因為實現了介面還可以繼續繼承。繼承Thread類不能再繼承。
有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行
/**
* @classDesc: 面試題:有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行
* 使用join()方法
* @author: hj
* @date:2018年12月11日 下午11:54:49
*/
public class JoinThreadDemo02 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("t1,i:" + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
t1.join();
} catch (Exception e) {
}
for (int i = 0; i < 20; i++) {
System.out.println("t2,i:" + i);
}
}
});
Thread t3 = new Thread(new Runnable() {
public void run() {
try {
t2.join();
} catch (Exception e) {
}
for (int i = 0; i < 20; i++) {
System.out.println("t3,i:" + i);
}
}
});
t1.start();
t2.start();
t3.start();
}
}