版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
一:爲什麼要學多線程
- 高級面試 :多線程幾乎是面試中必問的題,所以掌握一定的基礎知識是必須的。
- 瞭解併發編程:實際工作中很少寫多線程的代碼,這部分代碼一般都被人封裝起來了,在業務中使用多線程的機會也不是很多(看具體項目),雖然代碼中很少會自己去創建線程,但是實際環境中每行代碼卻都是並行執行的,同一時刻大量請求同一個接口,併發可能會產生一些問題,所以也需要掌握一定的併發知識
二:進程與線程
1. 進程
進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。程序運行時系統就會創建一個進程,併爲它分配資源,然後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。
2. 線程
線程是一條執行路徑,是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。
一個正在運行的軟件(如迅雷)就是一個進程,一個進程可以同時運行多個任務( 迅雷軟件可以同時下載多個文件,每個下載任務就是一個線程), 可以簡單的認爲進程是線程的集合。
線程是一條可以執行的路徑。多線程就是同時有多條執行路徑在同時(並行)執行。
3. 進程與線程的關係
一個程序就是一個進程,而一個程序中的多個任務則被稱爲線程。進程是表示資源分配的基本單位,又是調度運行的基本單位。,亦即執行處理機調度的基本單位。 進程和線程的關係:
-
一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。
-
資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量,即每個線程都有自己的堆棧和局部變量。
-
處理機分給線程,即真正在處理機上運行的是線程。
-
線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。
如果把上課的過程比作進程,把老師比作CPU,那麼可以把每個學生比作每個線程,所有學生共享這個教室(也就是所有線程共享進程的資源),上課時學生A向老師提出問題,老師對A進行解答,此時可能會有學生B對老師的解答不懂會提出B的疑問(注意:此時可能老師還沒有對A同學的問題解答完畢),此時老師又向學生B解惑,解釋完之後又繼續回答學生A的問題,同一時刻老師只能向一個學生回答問題(即:當多個線程在運行時,同一個CPU在某一個時刻只能服務於一個線程,可能一個線程分配一點時間,時間到了就輪到其它線程執行了,這樣多個線程在來回的切換)
4. 爲什麼要使用多線程
多線程可以提高程序的效率。
實際生活案例:村長要求喜洋洋在一個小時內打100桶水,可以喜洋洋一個小時只能打25桶水,如果這樣就需要4個小時才能完成任務,爲了在一個小時能夠完成,喜洋洋就請美洋洋、懶洋洋、沸洋洋,來幫忙,這樣4只羊同時幹活,在一小時內完成了任務。原本用4個小時完成的任務現在只需要1個小時就完成了,如果把每隻羊看做一個線程,多隻羊即多線程可以提高程序的效率。
5. 多線程應用場景
- 一般線程之間比較獨立,互不影響
- 一個線程發生問題,一般不影響其它線程
三:多線程的實現方式
1. 順序編程
順序編程:程序從上往下的同步執行,即如果第一行代碼執行沒有結束,第二行代碼就只能等待第一行執行結束後才能結束。
public class Main {
// 順序編程 吃喝示例:當吃飯吃不完的時候,是不能喝酒的,只能吃完晚才能喝酒
public static void main(String[] args) throws Exception {
// 先吃飯再喝酒
eat();
drink();
}
private static void eat() throws Exception {
System.out.println("開始吃飯?...\t" + new Date());
Thread.sleep(5000);
System.out.println("結束吃飯?...\t" + new Date());
}
private static void drink() throws Exception {
System.out.println("開始喝酒?️...\t" + new Date());
Thread.sleep(5000);
System.out.println("結束喝酒?...\t" + new Date());
}
}
2. 併發編程
併發編程:多個任務可以同時做,常用與任務之間比較獨立,互不影響。
線程上下文切換:
同一個時刻一個CPU只能做一件事情,即同一時刻只能一個線程中的部分代碼,假如有兩個線程,Thread-0和Thread-1,剛開始CPU說Thread-0你先執行,給你3毫秒時間,Thread-0執行了3毫秒時間,但是沒有執行完,此時CPU會暫停Thread-0執行並記錄Thread-0執行到哪行代碼了,當時的變量的值是多少,然後CPU說Thread-1你可以執行了,給你2毫秒的時間,Thread-1執行了2毫秒也沒執行完,此時CPU會暫停Thread-1執行並記錄Thread-1執行到哪行代碼了,當時的變量的值是多少,此時CPU又說Thread-0又該你,這次我給你5毫秒時間,去執行吧,此時CPU就找出上次Thread-0線程執行到哪行代碼了,當時的變量值是多少,然後接着上次繼續執行,結果用了2毫秒就Thread-0就執行完了,就終止了,然後CPU說Thread-1又輪到你,這次給你4毫秒,同樣CPU也會先找出上次Thread-1線程執行到哪行代碼了,當時的變量值是多少,然後接着上次繼續開始執行,結果Thread-1在4毫秒內也執行結束了,Thread-1也結束了終止了。CPU在來回改變線程的執行機會稱之爲線程上下文切換。
public class Main {
public static void main(String[] args) {
// 一邊吃飯一邊喝酒
new EatThread().start();
new DrinkThread().start();
}
}
class EatThread extends Thread{
@Override
public void run() {
System.out.println("開始吃飯?...\t" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("結束吃飯?...\t" + new Date());
}
}
class DrinkThread extends Thread {
@Override
public void run() {
System.out.println("開始喝酒?️...\t" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("結束喝酒?...\t" + new Date());
}
}
併發編程,一邊吃飯一邊喝酒總共用時5秒,比順序編程更快,因爲併發編程可以同時運行,而不必等前面的代碼運行完之後才允許後面的代碼
本示例主要啓動3個線程,一個主線程main thread、一個吃飯線程(Thread-0)和一個喝酒線程(Thread-1),共三個線程, 三個線程併發切換着執行。main線程很快執行完,吃飯線程和喝酒線程會繼續執行,直到所有線程(非守護線程)執行完畢,整個程序纔會結束,main線程結束並不意味着整個程序結束。
-
順序:代碼從上而下按照固定的順序執行,只有上一件事情執行完畢,才能執行下一件事。就像物理電路中的串行,假如有十件事情,一個人來完成,這個人必須先做第一件事情,然後再做第二件事情,最後做第十件事情,按照順序做。
-
並行:多個操作同時處理,他們之間是並行的。假如十件事情,兩個人來完成,每個人在某個時間點各自做各自的事情,互不影響
-
併發:將一個操作分割成多個部分執行並且允許無序處理,假如有十件事情,如果有一個人在做,這個人可能做一會這個不想做了,再去做別的,做着做着可能也不想做了,又去幹其它事情了,看他心情想幹哪個就幹哪個,最終把十件事情都做完。如果有兩個人在做,他們倆先分一下,比如張三做4件,李四做6件,他們各做自己的,在做自己的事情過程中可以隨意的切換到別的事情,不一定要把某件事情幹完再去幹其它事情,有可能一件事做了N次才做完。
通常一臺電腦只有一個cpu,多個線程屬於併發執行,如果有多個cpu,多線程併發執行有可能變成並行執行。
3. 多線程創建方式
- 繼承 Thread
- 實現 Runable
- 實現 Callable
①:繼成java.lang.Thread, 重寫run()方法
public class Main {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}
Thread 類
package java.lang;
public class Thread implements Runnable {
// 構造方法
public Thread(Runnable target);
public Thread(Runnable target, String name);
public synchronized void start();
}
Runnable 接口
package java.lang;
@FunctionalInterface
public interface Runnable {
pubic abstract void run();
}
②:實現java.lang.Runnable接口,重寫run()方法,然後使用Thread類來包裝
public class Main {
public static void main(String[] args) {
// 將Runnable實現類作爲Thread的構造參數傳遞到Thread類中,然後啓動Thread類
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}
可以看到兩種方式都是圍繞着Thread和Runnable,繼承Thread類把run()寫到類中,實現Runnable接口是把run()方法寫到接口中然後再用Thread類來包裝, 兩種方式最終都是調用Thread類的start()方法來啓動線程的。
兩種方式在本質上沒有明顯的區別,在外觀上有很大的區別,第一種方式是繼承Thread類,因Java是單繼承,如果一個類繼承了Thread類,那麼就沒辦法繼承其它的類了,在繼承上有一點受制,有一點不靈活,第二種方式就是爲了解決第一種方式的單繼承不靈活的問題,所以平常使用就使用第二種方式
其它變體寫法:
public class Main {
public static void main(String[] args) {
// 匿名內部類
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}).start();
// 尾部代碼塊, 是對匿名內部類形式的語法糖
new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}.start();
// Runnable是函數式接口,所以可以使用Lamda表達式形式
Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
new Thread(runnable).start();
}
}
③:實現Callable接口,重寫call()方法,然後包裝成java.util.concurrent.FutureTask, 再然後包裝成Thread
Callable:有返回值的線程,能取消線程,可以判斷線程是否執行完畢
public class Main {
public static void main(String[] args) throws Exception {
// 將Callable包裝成FutureTask,FutureTask也是一種Runnable
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法會阻塞調用的線程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
return sum;
}
}
Callable 也是一種函數式接口
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {
// 構造函數
public FutureTask(Callable<V> callable);
// 取消線程
public boolean cancel(boolean mayInterruptIfRunning);
// 判斷線程
public boolean isDone();
// 獲取線程執行結果
public V get() throws InterruptedException, ExecutionException;
}
RunnableFuture
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
三種方式比較:
- Thread: 繼承方式, 不建議使用, 因爲Java是單繼承的,繼承了Thread就沒辦法繼承其它類了,不夠靈活
- Runnable: 實現接口,比Thread類更加靈活,沒有單繼承的限制
- Callable: Thread和Runnable都是重寫的run()方法並且沒有返回值,Callable是重寫的call()方法並且有返回值並可以藉助FutureTask類來判斷線程是否已經執行完畢或者取消線程執行
- 當線程不需要返回值時使用Runnable,需要返回值時就使用Callable,一般情況下不直接把線程體代碼放到Thread類中,一般通過Thread類來啓動線程
- Thread類是實現Runnable,Callable封裝成FutureTask,FutureTask實現RunnableFuture,RunnableFuture繼承Runnable,所以Callable也算是一種Runnable,所以三種實現方式本質上都是Runnable實現
四:線程的狀態
- 創建(new)狀態: 準備好了一個多線程的對象,即執行了new Thread(); 創建完成後就需要爲線程分配內存
- 就緒(runnable)狀態: 調用了start()方法, 等待CPU進行調度
- 運行(running)狀態: 執行run()方法
- 阻塞(blocked)狀態: 暫時停止執行線程,將線程掛起(sleep()、wait()、join()、沒有獲取到鎖都會使線程阻塞), 可能將資源交給其它線程使用
- 死亡(terminated)狀態: 線程銷燬(正常執行完畢、發生異常或者被打斷interrupt()都會導致線程終止)
五:Thread常用方法
Thread
public class Thread implements Runnable {
// 線程名字
private volatile String name;
// 線程優先級(1~10)
private int priority;
// 守護線程
private boolean daemon = false;
// 線程id
private long tid;
// 線程組
private ThreadGroup group;
// 預定義3個優先級
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
// 構造函數
public Thread();
public Thread(String name);
public Thread(Runnable target);
public Thread(Runnable target, String name);
// 線程組
public Thread(ThreadGroup group, Runnable target);
// 返回當前正在執行線程對象的引用
public static native Thread currentThread();
// 啓動一個新線程
public synchronized void start();
// 線程的方法體,和啓動線程沒毛關係
public void run();
// 讓線程睡眠一會,由活躍狀態改爲掛起狀態
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;
// 打斷線程 中斷線程 用於停止線程
// 調用該方法時並不需要獲取Thread實例的鎖。無論何時,任何線程都可以調用其它線程的interruptf方法
public void interrupt();
public boolean isInterrupted()
// 線程是否處於活動狀態
public final native boolean isAlive();
// 交出CPU的使用權,從運行狀態改爲掛起狀態
public static native void yield();
public final void join() throws InterruptedException
public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos) throws InterruptedException
// 設置線程優先級
public final void setPriority(int newPriority);
// 設置是否守護線程
public final void setDaemon(boolean on);
// 線程id
public long getId() { return this.tid; }
// 線程狀態
public enum State {
// new 創建
NEW,
// runnable 就緒
RUNNABLE,
// blocked 阻塞
BLOCKED,
// waiting 等待
WAITING,
// timed_waiting
TIMED_WAITING,
// terminated 結束
TERMINATED;
}
}
public static void main(String[] args) {
// main方法就是一個主線程
// 獲取當前正在運行的線程
Thread thread = Thread.currentThread();
// 線程名字
String name = thread.getName();
// 線程id
long id = thread.getId();
// 線程優先級
int priority = thread.getPriority();
// 是否存活
boolean alive = thread.isAlive();
// 是否守護線程
boolean daemon = thread.isDaemon();
// Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
}
0. Thread.currentThread()
public static void main(String[] args) {
Thread thread = Thread.currentThread();
// 線程名稱
String name = thread.getName();
// 線程id
long id = thread.getId();
// 線程已經啓動且尚未終止
// 線程處於正在運行或準備開始運行的狀態,就認爲線程是“存活”的
boolean alive = thread.isAlive();
// 線程優先級
int priority = thread.getPriority();
// 是否守護線程
boolean daemon = thread.isDaemon();
// Thread[name=main,id=1,alive=true,priority=5,daemon=false]
System.out.println("Thread[name=" + name + ",id=" + id + ",alive=" + alive + ",priority=" + priority + ",daemon=" + daemon + "]");
}
1. start() 與 run()
public static void main(String[] args) throws Exception {
new Thread(()-> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
}, "Thread-A").start();
new Thread(()-> {
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName() + " " + j);
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
}, "Thread-B").start();
}
start(): 啓動一個線程,線程之間是沒有順序的,是按CPU分配的時間片來回切換的。
public static void main(String[] args) throws Exception {
new Thread(()-> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
}, "Thread-A").run();
new Thread(()-> {
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName() + " " + j);
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
}, "Thread-B").run();
}
注意:執行結果都是main主線程
run(): 調用線程的run方法,就是普通的方法調用,雖然將代碼封裝到兩個線程體中,可以看到線程中打印的線程名字都是main主線程,run()方法用於封裝線程的代碼,具體要啓動一個線程來運行線程體中的代碼(run()方法)還是通過start()方法來實現,調用run()方法就是一種順序編程不是併發編程。
有些面試官經常問一些啓動一個線程是用start()方法還是run()方法,爲了面試而面試。
2. sleep() 與 interrupt()
public static native void sleep(long millis) throws InterruptedException;
public void interrupt();
sleep(long millis): 睡眠指定時間,程序暫停運行,睡眠期間會讓出CPU的執行權,去執行其它線程,同時CPU也會監視睡眠的時間,一旦睡眠時間到就會立刻執行(因爲睡眠過程中仍然保留着鎖,有鎖只要睡眠時間到就能立刻執行)。
- sleep(): 睡眠指定時間,即讓程序暫停指定時間運行,時間到了會繼續執行代碼,如果時間未到就要醒需要使用interrupt()來隨時喚醒
- interrupt(): 喚醒正在睡眠的程序,調用interrupt()方法,會使得sleep()方法拋出InterruptedException異常,當sleep()方法拋出異常就中斷了sleep的方法,從而讓程序繼續運行下去
public static void main(String[] args) throws Exception {
Thread thread0 = new Thread(()-> {
try {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,讓我睡10秒,中間有事叫我,zZZ。。。");
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要繼續幹活了");
}
});
thread0.start();
// 這裏睡眠只是爲了保證先讓上面的那個線程先執行
Thread.sleep(2000);
new Thread(()-> {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,別睡了,起來幹活了!!!");
// 無需獲取鎖就可以調用interrupt
thread0.interrupt();
}).start();
}
3. wait() 與 notify()
wait、notify和notifyAll方法是Object類的final native方法。所以這些方法不能被子類重寫,Object類是所有類的超類,因此在程序中可以通過this或者super來調用this.wait(), super.wait()
- wait(): 導致線程進入等待阻塞狀態,會一直等待直到它被其他線程通過notify()或者notifyAll喚醒。該方法只能在同步方法中調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。wait(long timeout): 時間到了自動執行,類似於sleep(long millis)
- notify(): 該方法只能在同步方法或同步塊內部調用, 隨機選擇一個(注意:只會通知一個)在該對象上調用wait方法的線程,解除其阻塞狀態
- notifyAll(): 喚醒所有的wait對象
注意:
- Object.wait()和Object.notify()和Object.notifyall()必須寫在synchronized方法內部或者synchronized塊內部
- 讓哪個對象等待wait就去通知notify哪個對象,不要讓A對象等待,結果卻去通知B對象,要操作同一個對象
Object
public class Object {
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
}
WaitNotifyTest
public class WaitNotifyTest {
public static void main(String[] args) throws Exception {
WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
new Thread(() -> {
try {
waitNotifyTest.printFile();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
waitNotifyTest.printFile();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t睡覺1秒中,目的是讓上面的線程先執行,即先執行wait()");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitNotifyTest.notifyPrint();
}).start();
}
private synchronized void printFile() throws InterruptedException {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
this.wait();
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印結束。。。");
}
private synchronized void notifyPrint() {
this.notify();
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成...");
}
}
wait():讓程序暫停執行,相當於讓當前,線程進入當前實例的等待隊列,這個隊列屬於該實例對象,所以調用notify也必須使用該對象來調用,不能使用別的對象來調用。調用wait和notify必須使用同一個對象來調用。
this.notifyAll();
4. sleep() 與 wait()
① Thread.sleep(long millis): 睡眠時不會釋放鎖
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
因main方法中Thread.sleep(1000)所以上面的線程Thread-0先被執行,當循環第一次時就會Thread.sleep(1000)睡眠,因爲sleep並不會釋放鎖,所以Thread-1得不到執行的機會,所以直到Thread-0執行完畢釋放鎖對象lock,Thread-1才能拿到鎖,然後執行Thread-1;
5. wait() 與 interrupt()
wait(): 方法的作用是釋放鎖,加入到等待隊列,當調用interrupt()方法後,線程必須先獲取到鎖後,然後才拋出異常InterruptedException 。注意: 在獲取鎖之前是不會拋出異常的,只有在獲取鎖之後纔會拋異常
所有能拋出InterruptedException的方法都可以通過interrupt()來取消的
public static native void sleep(long millis) throws InterruptedException;
public final void wait() throws InterruptedException;
public final void join() throws InterruptedException;
public void interrupt();
notify()和interrupt()
從讓正在wait的線程重新運行這一點來說,notify方法和intterrupt方法的作用有些類似,但仍有以下不同之處:
-
notify/notifyAll是java.lang.Object類的方法,喚醒的是該實例的等待隊列中的線程,而不能直接指定某個具體的線程。notify/notifyAll喚醒的線程會繼續執行wait的下一條語句,另外執行notify/notifyAll時線程必須要獲取實例的鎖
-
interrupte方法是java.lang.Thread類的方法,可以直接指定線程並喚醒,當被interrupt的線程處於sleep或者wait中時會拋出InterruptedException異常。執行interrupt()並不需要獲取取消線程的鎖。
-
總之notify/notifyAll和interrupt的區別在於是否能直接讓某個指定的線程喚醒、執行喚醒是否需要鎖、方法屬於的類不同
6. interrupt()
有人也許認爲“當調用interrupt方法時,調用對象的線程就會InterruptedException異常”, 其實這是一種誤解,實際上interrupt方法只是改變了線程的“中斷狀態”而已,所謂中斷狀態是一個boolean值,表示線程是否被中斷的狀態。
public class Thread implements Runnable {
public void interrupt() {
中斷狀態 = true;
}
// 檢查中斷狀態
public boolean isInterrupted();
// 檢查中斷狀態並清除當前線程的中斷狀態
public static boolean interrupted() {
// 僞代碼
boolean isInterrupted = isInterrupted();
中斷狀態 = false;
}
}
假設Thread-0執行了sleep、wait、join中的一個方法而停止運行,在Thread-1中調用了interrupt方法,此時線程Thread-0的確會拋出InterruptedException異常,但這其實是sleep、wait、join中的方法內部會對線程的“中斷狀態”進行檢查,如果中斷狀態爲true,就會拋出InterruptedException異常。假如某個線程的中斷狀態爲true,但線程體中卻沒有調用或者沒有判斷線程中斷狀態的值,那麼線程則不會拋出InterruptedException異常。
isInterrupted() 檢查中斷狀態
若指定線程處於中斷狀態則返回true,若指定線程爲非中斷狀態,則反回false, isInterrupted() 只是獲取中斷狀態的值,並不會改變中斷狀態的值。
interrupted()
檢查中斷狀態並清除當前線程的中斷狀態。如當前線程處於中斷狀態返回true,若當前線程處於非中斷狀態則返回false, 並清除中斷狀態(將中斷狀態設置爲false), 只有這個方法纔可以清除中斷狀態,Thread.interrupted的操作對象是當前線程,所以該方法並不能用於清除其它線程的中斷狀態。
interrupt()與interrupted()
- interrupt():打斷線程,將中斷狀態修改爲true
- interrupted(): 不打斷線程,獲取線程的中斷狀態,並將中斷狀態設置爲false
public class InterrupptTest {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
boolean interrupted = thread.isInterrupted();
// interrupted=false
System.out.println("interrupted=" + interrupted);
thread.interrupt();
boolean interrupted2 = thread.isInterrupted();
// interrupted2=true
System.out.println("interrupted2=" + interrupted2);
boolean interrupted3 = Thread.interrupted();
// interrupted3=false
System.out.println("interrupted3=" + interrupted3);
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
// InterruptedException false
System.out.println("InterruptedException\t" + Thread.currentThread().isInterrupted());
}
}
}
}
② object.wait(long timeout): 會釋放鎖
public class SleepWaitTest {
public static void main(String[] args) throws InterruptedException {
SleepWaitTest object = new SleepWaitTest();
new Thread(() -> {
synchronized (object) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
try {
object.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印結束。。。");
}
}).start();
// 先上面的線程先執行
Thread.sleep(1000);
new Thread(() -> {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}
因main方法中有Thread.sleep(1000)所以上面的線程Thread-0肯定會被先執行,當Thread-0被執行時就拿到了object對象鎖,然後進入wait(5000)5秒鐘等待,此時wait釋放了鎖,然後Thread-1就拿到了鎖就執行線程體,Thread-1執行完後就釋放了鎖,當等待5秒後Thread-0就能再次獲取object鎖,這樣就繼續執行後面的代碼。wait方法是釋放鎖的,如果wait方法不釋放鎖那麼Thread-1是拿不到鎖也就沒有執行的機會的,事實是Thread-1得到了執行,所以說wait方法會釋放鎖
③ sleep與wait的區別
- sleep在Thread類中,wait在Object類中
- sleep不會釋放鎖,wait會釋放鎖
- sleep使用interrupt()來喚醒,wait需要notify或者notifyAll來通知
5.join()
讓當前線程加入父線程,加入後父線程會一直wait,直到子線程執行完畢後父線程才能執行。當我們調用某個線程的這個方法時,這個方法會掛起調用線程,直到被調用線程結束執行,調用線程纔會繼續執行。
將某個線程加入到當前線程中來,一般某個線程和當前線程依賴關係比較強,必須先等待某個線程執行完畢才能執行當前線程。一般在run()方法內使用
join() 方法:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 循環檢查線程的狀態是否還活着,如果死了就結束了,如果活着繼續等到死
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
public final synchronized void join(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
JoinTest
public class JoinTest {
public static void main(String[] args) {
new Thread(new ParentRunnable()).start();
}
}
class ParentRunnable implements Runnable {
@Override
public void run() {
// 線程處於new狀態
Thread childThread = new Thread(new ChildRunable());
// 線程處於runnable就緒狀態
childThread.start();
try {
// 當調用join時,parent會等待child執行完畢後再繼續運行
// 將某個線程加入到當前線程
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父線程 running");
}
}
}
class ChildRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子線程 running");
}
}
}
程序進入主線程,運行Parent對應的線程,Parent的線程代碼分兩段,一段是啓動一個子線程,一段是Parent線程的線程體代碼,首先會將Child線程加入到Parent線程,join()方法會調用join(0)方法(join()方法是普通方法並沒有加鎖,join(0)會加鎖),join(0)會執行while(isAlive()) { wait(0);} 循環判斷線程是否處於活動狀態,如果是繼續wait(0)知道isAlive=false結束掉join(0), 從而結束掉join(), 最後回到Parent線程體中繼續執行其它代碼。
在Parent調用child.join()後,child子線程正常運行,Parent父線程會等待child子線程結束後再繼續運行。
-
join() 和 join(long millis, int nanos) 最後都調用了 join(long millis)。
-
join(long millis, int nanos)和join(long millis)方法 都是synchronized。
-
join() 調用了join(0),從源碼可以看到join(0)不斷檢查當前線程是否處於Active狀態。
-
join() 和 sleep() 一樣,都可以被中斷(被中斷時,會拋出 InterrupptedException 異常);不同的是,join() 內部調用了wait(),會出讓鎖,而 sleep() 會一直保持鎖。
6. yield()
交出CPU的執行時間,不會釋放鎖,讓線程進入就緒狀態,等待重新獲取CPU執行時間,yield就像一個好人似的,當CPU輪到它了,它卻說我先不急,先給其他線程執行吧, 此方法很少被使用到,
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
public static void main(String[] args) {
new Thread(new Runnable() {
int sum = 0;
@Override
public void run() {
long beginTime=System.currentTimeMillis();
for (int i = 0; i < 99999; i++) {
sum += 1;
// 去掉該行執行用2毫秒,加上271毫秒
Thread.yield();
}
long endTime=System.currentTimeMillis();
System.out.println("用時:"+ (endTime - beginTime) + " 毫秒!");
}
}).start();
}
sleep(long millis) 與 yeid()
- sleep(long millis): 需要指定具體睡眠的時間,不會釋放鎖,睡眠期間CPU會執行其它線程,睡眠時間到會立刻執行
- yeid(): 交出CPU的執行權,不會釋放鎖,和sleep不同的時當再次獲取到CPU的執行,不能確定是什麼時候,而sleep是能確定什麼時候再次執行。兩者的區別就是sleep後再次執行的時間能確定,而yeid是不能確定的
- yield會把CPU的執行權交出去,所以可以用yield來控制線程的執行速度,當一個線程執行的比較快,此時想讓它執行的稍微慢一些可以使用該方法,想讓線程變慢可以使用sleep和wait,但是這兩個方法都需要指定具體時間,而yield不需要指定具體時間,讓CPU決定什麼時候能再次被執行,當放棄到下次再次被執行的中間時間就是間歇等待的時間
7. setDaemon(boolean on)
線程分兩種:
- 用戶線程:如果主線程main停止掉,不會影響用戶線程,用戶線程可以繼續運行。
- 守護線程:如果主線程死亡,守護線程如果沒有執行完畢也要跟着一塊死(就像皇上死了,帶刀侍衛也要一塊死),GC垃圾回收線程就是守護線程
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
});
}
};
thread.start();
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
}
System.out.println("主線程執行結束,子線程仍然繼續執行,主線程和用戶線程的生命週期各自獨立。");
}
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
});
}
};
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\ti=" + i);
}
System.out.println("主線程死亡,子線程也要陪着一塊死!");
}
六 線程組
可以對線程分組,分組後可以統一管理某個組下的所有線程,例如統一中斷所有線程
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
String name;
int maxPriority;
Thread threads[];
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
// 返回此線程組中活動線程的估計數。
public int activeGroupCount();
// 中斷此線程組中的所有線程。
public final void interrupt();
}
public static void main(String[] args) {
String mainThreadGroupName = Thread.currentThread().getThreadGroup().getName();
System.out.println(mainThreadGroupName);
// 如果一個線程沒有指定線程組,默認爲當前線程所在的線程組
new Thread(() -> { }, "my thread1").start();
ThreadGroup myGroup = new ThreadGroup("MyGroup");
myGroup.setMaxPriority(5);
Runnable runnable = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
String groupName = threadGroup.getName();
ThreadGroup parentGroup = threadGroup.getParent();
String parentGroupName = parentGroup.getName();
ThreadGroup grandpaThreadGroup = parentGroup.getParent();
String grandpaThreadGroupName = grandpaThreadGroup.getName();
int maxPriority = threadGroup.getMaxPriority();
int activeCount = myGroup.activeCount();
// system <- main <- MyGroup(1) <- my thread2
System.out.println(MessageFormat.format("{0} <- {1} <- {2}({3}) <- {4}",
grandpaThreadGroupName,
parentGroupName,
groupName,
activeCount,
Thread.currentThread().getName()));
};
new Thread(myGroup, runnable, "my thread2").start();
}
線程組與線程組之間是有父子關係的,自定義線程組的父線程組是main線程組,main線程組的父線程組是system線程組。