一、多線程創建方式
1.1、繼承Thread類創建線程類
1.實現步驟
-
定義一個繼承Thread類的子類,並重寫該類的run()方法;
-
創建Thread子類的實例,即創建了線程對象;
-
調用該線程對象的start()方法啟動線程。
2.核心代碼
class SomeThead extends Thraad {
public void run() {
//do something here
}
}
public static void main(String[] args){
SomeThread oneThread = new SomeThread();
//啟動線程
oneThread.start();
}
1.2、實現Runnable接口創建線程類
1.實現步驟
-
定義Runnable接口的實現類,並重寫該接口的run()方法;
-
創建Runnable實現類的實例,並以此實例作為Thread的target對象,即該Thread對象才是真正的線程對象。
2.核心代碼
class SomeRunnable implements Runnable {
public void run() {
//do something here
}
}
Runnable oneRunnable = new SomeRunnable();
Thread oneThread = new Thread(oneRunnable);
oneThread.start();
1.3、通過Callable和Future創建線程
1.實現步驟
-
創建Callable接口的實現類,並實現call()方法,改方法將作為線程執行體,且具有返回值。
-
創建Callable實現類的實例,使用FutrueTask類進行包裝Callable對象,FutureTask對象封裝了Callable對象的call()方法的返回值
-
使用FutureTask對象作為Thread對象的target創建並啟動新線程
-
調用FutureTask對象的get()方法獲取子線程執行結束后的返回值。
2.核心代碼
//1.創建Callable接口的實現類,並實現call()方法
public class SomeCallable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
for(;i<10;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
public static void main(String[] args) {
//2.創建Callable實現類的實例
SomeCallable01 ctt = new SomeCallable01();
//3.使用FutrueTask類進行包裝Callable對象,FutureTask對象封裝了Callable對象的call()方法的返回值
FutureTask<Integer> ft = new FutureTask<>(ctt);
//開啟ft線程
for(int i = 0;i < 21;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
if(i==20)//i為20的時候創建ft線程
{
//4.使用FutureTask對象作為Thread對象的target創建並啟動新線程
new Thread(ft,"有返回值的線程FutureTask").start();
}
}
//ft線程結束時,獲取返回值
try
{
//5.調用FutureTask對象的get()方法獲取子線程執行結束后的返回值。
System.out.println("子線程的返回值:"+ft.get());//get()方法會阻塞,直到子線程執行結束才返回
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
}
二、創建線程方式的區別
1.使用繼承Thread類的方式創建多線程
1)優勢
編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。
2)劣勢
線程類已經繼承了Thread類,所以不能再繼承其他父類。(有單繼承的局限性)
創建多線程時,每個任務有成員變量時不共享,必須加static才能做到共享
2.使用實現Runnable類的方式創建多線程
1)優勢
避免了單繼承的局限性、多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。
2)劣勢
比較復雜、訪問線程必須使用Thread.currentThread()方法、無返回值。
3.使用實現Callable接口的方式創建多線程
1)優勢
有返回值、避免了單繼承的局限性、多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。
2)劣勢
比較復雜、訪問線程必須使用Thread.currentThread()方法
4.Runnable和Callable的區別
1)Callable規定(重寫)的方法是call(),Runnable規定(重寫)的方法是run()。
2)Callable的任務執行后可返回值,而Runnable的任務是不能返回值的。
3)call方法可以拋出異常,run方法不可以。
4)運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的
完成,並檢索計算的結果。通過Future對象可以了解任務執行情況,可取消任務的執行,還可獲取執行結果future.get()。
三、多線程調度
3.1、調度策略
時間片:線程的調度采用時間片輪轉的方式
搶占式:高優先級的線程搶占CPU
3.2、Java的調度方法
1)對於同優先級的線程組成先進先出隊列(先到先服務),使用時間片策略
2)對高優先級,使用優先調度的搶占式策略
3.3、線程的優先級
等級:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回線程優先級
setPriority(int newPriority):改變線程的優先級
備注:
高優先級的線程要搶占低優先級的線程的cpu的執行權。但是僅是從概率上來說的,高優先級的線程更有可能被執行。並不意味着只有高優先級的線程執行完以后,低優先級的線程才執行。
四、多線程狀態管理
4.1、線程睡眠---sleep
1)概述
如果我們需要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過調用Thread的sleep方法。
2)線程睡眠方法
在指定的毫秒數內讓正在執行的線程休眠:
sleep(long millis)
在指定的毫秒數加指定的納秒數內讓正在執行的線程休眠:
sleep(long millis,int nanos)
3)代碼實現
sleep是靜態方法,最好不要用Thread的實例對象調用它,因為它睡眠的始終是當前正在運行的線程,而不是調用它的線程對象,它只對正在運行狀態的線程對象有效。
public class SynTest {
public static void main(String[] args) {
new Thread(new CountDown(),"倒計時").start();
}
}
class CountDown implements Runnable{
int time = 10;
public void run() {
while (true) {
if(time>=0){
System.out.println(Thread.currentThread().getName() + ":" + time--);
try {
Thread.sleep(1000); //睡眠時間為1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
4)備注
Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程序的執行效率。但是不管程序員怎么編寫調度,只能最大限度的影響線程執行的次序,而不能做到精准控制。因為使用sleep方法之后,線程是進入阻塞狀態的,只有當睡眠的時間結束,才會重新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,我們不可能精准的去干涉它,所以如果調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。
4.2、線程讓步---yield
1)概述
yield()方法和sleep()方法有點相似,它也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的線程暫停,讓出cpu資源給其他的線程。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前線程暫停一下,重新進入就緒的線程池中,讓系統的線程調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個線程調用yield()方法之后,線程調度器又將其調度出來重新進入到運行狀態執行。
實際上,當某個線程調用了yield()方法暫停之后,優先級與當前線程相同,或者優先級比當前線程更高的就緒狀態的線程更有可能獲得執行的機會,當然,只是有可能,因為我們不可能精確的干涉cpu調度線程。
2)代碼實現
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低級", 1).start();
new MyThread("中級", 5).start();
new MyThread("高級", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 設置線程的名稱
this.setPriority(pro);// 設置優先級
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "線程第" + i + "次執行!");
if (i % 5 == 0)
Thread.yield();
}
}
}
3)sleep和yield的區別
① sleep方法暫停當前線程后,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。而yield方法調用后 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態。
② sleep方法聲明拋出了InterruptedException,所以調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務異常。
③ sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制並發線程的執行。
4.3、線程合並---join
1)概述
線程的合並的含義就是將幾個並行線程的線程合並為一個單線程執行,應用場景是當一個線程必須等待另一個線程執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。
簡而言之:
當B線程執行到了A線程的.join()方法時,B線程就會等待,等A線程都執行完畢,B線程才會執行。join可以用來臨時加入線程執行。
2)線程合並方法
它有三個重載方法:
當前線程等該加入該線程后面,等待該線程終止。
void join()
當前線程等待該線程終止的時間最長為 millis 毫秒。
如果在millis時間內,該線程沒有執行完,那么當前線程進入就緒狀態,重新等待cpu調度
void join(long millis)
等待該線程終止的時間最長為 millis 毫秒 + nanos
納秒。如果在millis時間內,該線程沒有執行完,那么當前線程進入就緒狀態,重新等待cpu調度
void join(long millis,int nanos)
3)代碼實現
public static void main(String[] args) throws InterruptedException {
yieldDemo ms = new yieldDemo();
Thread t1 = new Thread(ms,"張三吃完還剩");
Thread t2 = new Thread(ms,"李四吃完還剩");
Thread t3 = new Thread(ms,"王五吃完還剩");
t1.start();
t1.join();
t2.start();
t3.start();
System.out.println( "主線程");
}
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t.start();
// 讓主線程阻塞 等待t線程執行完才繼續執行
// 去除該行,執行結果為0,加上該行 執行結果為10
t.join();
log.info("r:{}", r);
// 運行結果
13:09:13.892 [main] INFO thread.TestJoin - r:10
4.4、設置線程的優先級
1)概述
每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的也並非沒機會執行。
每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級。
2)涉及優先級方法
Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參數是一個整數,范圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
3)代碼實現
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高級", 10).start();
new MyThread("低級", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//設置線程的名稱
setPriority(pro);//設置線程的優先級
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "線程第" + i + "次執行!");
}
}
}
4)備注
雖然Java提供了10個優先級別,但這些優先級別需要操作系統的支持。不同的操作系統的優先級並不相同,而且也不能很好的和Java的10個優先級別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先級,這樣才能保證程序最好的可移植性。
4.5、后台(守護)線程
1)概述
守護線程使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含着很多后台線程,監控連接個數、超時時間、狀態等等。
默認情況下,java進程需要等待所有線程都運行結束,才會結束,有一種特殊線程叫守護線程,當所有的非守護線程都結束后,即使它沒有執行完,也會強制結束。
2)涉及方法
調用線程對象的方法setDaemon(true),則可以將其設置為守護線程。
將該線程標記為守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。
該方法必須在啟動線程前調用。 該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。
public final void setDaemon(boolean on)
參數:
on - 如果為 true,則將該線程標記為守護線程。
拋出:
IllegalThreadStateException - 如果該線程處於活動狀態。
SecurityException - 如果當前線程無法修改該線程。
3)守護線程的用途
守護線程通常用於執行一些后台作業,例如在你的應用程序運行時播放背景音樂,在文字編輯器里做自動語法檢查、自動保存等功能。
java的垃圾回收也是一個守護線程。守護線的好處就是你不需要關心它的結束問題。例如你在你的應用程序運行的時候希望播放背景音樂,如果將這個播放背景音樂的線程設定為非守護線程,那么在用戶請求退出的時候,不僅要退出主線程,還要通知播放背景音樂的線程退出;如果設定為守護線程則不需要了。
4.6、停止線程
1)概述
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運行的方法已經被廢棄了,使用它們是極端不安全的。
正確停止線程的方法:
第一:正常執行完run方法,然后結束掉。
第二:控制循環條件和判斷條件的標識符來結束掉線程。
2)實現代碼示例
class MyThread extends Thread {
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
System.out.println(i);
}
}
}
4.7、線程打斷---interrupt
1)什么是中斷(interrupt)
中斷只是一種協作機制,Java沒有給中斷增加任何語法,中斷的過程完全需要程序員自己實現;
每個線程對象中都有一個標識,用於表示線程是否被中斷;該標識位為true表示中斷,為false表示未中斷;
通過調用線程對象的interrupt方法將該線程的標識位設為true;可以在別的線程中調用,也可以在自己的線程中調用。
打斷標記:線程是否被打斷,true表示被打斷了,false表示沒有
2)涉及方法
isInterrupted()方法:
獲取線程的打斷標記(哪個線程對象調用就檢查誰的) ,調用后不會修改線程的打斷標記
interrupt()方法:
中斷this線程(哪個線程對象調用即中斷誰)。如果這個需要被中斷線程處於阻塞狀態(sleep、wait、join),那么它的中斷狀態就會被清除,並且拋出異常(InterruptedException)。這個中斷並非真正的停止掉線程,而是將它的中斷狀態設置成“停止”的狀態,線程還是會繼續運行,至於怎么停止掉該線程,還是要靠我們自己去停止,該方法只是將線程的狀態設置成“停止”的狀態,即true。
打斷正常線程 ,線程不會真正被中斷,但是線程的打斷標記為true。
interrupted()方法:
檢查當前線程是否被中斷,與上面的interrupt()方法配合一起用。線程的中斷狀態將會被這個方法清除,也就是說:如果這個方法被連續成功調用兩次,第二次
調用將會返回false(除非當前線程在第一次調用之后和第二次調用之前又被中斷了)。
也就是說:調用后清空打斷標記 即如果獲取為true 調用后打斷標記為false (不常用)
4.8、線程堵塞
線程的阻塞可以分為好多種,從操作系統層面和java層面阻塞的定義可能不同,但是廣義上使得線程阻塞的方式有下面幾種:
1)BIO阻塞,即使用了阻塞式的io流
2)sleep(long time) 讓線程休眠進入阻塞狀態
3)a.join() 調用該方法的線程進入阻塞,等待a線程執行完恢復運行
4)sychronized或ReentrantLock 造成線程未獲得鎖進入阻塞狀態
5)獲得鎖之后調用wait()方法 也會讓線程進入阻塞狀態
6)LockSupport.park() 讓線程進入阻塞狀態
五、線程核心方法總結
5.1、六種線程狀態和方法的對應關系
5.2、線程核心方法總結
1)Thread類中的核心方法
方法名稱 | 是否static | 方法說明 |
---|---|---|
start() | 否 | 讓線程啟動,進入就緒狀態,等待cpu分配時間片 |
run() | 否 | 重寫Runnable接口的方法,線程獲取到cpu時間片時執行的具體邏輯 |
yield() | 是 | 線程的禮讓,使得獲取到cpu時間片的線程進入就緒狀態,重新爭搶時間片 |
sleep(time) | 是 | 線程休眠固定時間,進入阻塞狀態,休眠時間完成后重新爭搶時間片,休眠可被打斷 |
join()/join(time) | 否 | 調用線程對象的join方法,調用者線程進入阻塞,等待線程對象執行完或者到達指定時間才恢復,重新爭搶時間片 |
isInterrupted() | 否 | 獲取線程的打斷標記,true:被打斷,false:沒有被打斷。調用后不會修改打斷標記 |
interrupt() | 否 | 打斷線程,拋出InterruptedException異常的方法均可被打斷,但是打斷后不會修改打斷標記,正常執行的線程被打斷后會修改打斷標記 |
interrupted() | 否 | 獲取線程的打斷標記。調用后會清空打斷標記 |
stop() | 否 | 停止線程運行 不推薦 |
suspend() | 否 | 掛起線程 不推薦 |
resume() | 否 | 恢復線程運行 不推薦 |
currentThread() | 是 | 獲取當前線程 |
2)Object中與線程相關方法
方法名稱 | 方法說明 |
---|---|
wait()/wait(long timeout) | 獲取到鎖的線程進入阻塞狀態 |
notify() | 隨機喚醒被wait()的一個線程 |
notifyAll() | 喚醒被wait()的所有線程,重新爭搶時間片 |