Java線程創(chuàng)建與Thread類的使用方法
1.線程與Thread類
1.1操作系統(tǒng)中的線程與Java線程
1.1.1線程與Thread類
線程是操作系統(tǒng)中的概念. 操作系統(tǒng)內(nèi)核實現(xiàn)了線程這樣的機(jī)制, 并且對用戶層提供了一些 API 供用戶使用(例如 Linux 的 pthread 庫).
Java 標(biāo)準(zhǔn)庫中 Thread 類可以視為是對操作系統(tǒng)提供的 API 進(jìn)行了進(jìn)一步的抽象和封裝. 也就是說Thread
類的一個實例就對應(yīng)著一個線程。
1.1.2Thread類的構(gòu)造方法
序號 | 方法名 | 解釋 |
---|---|---|
1 | public Thread() | 無參數(shù)構(gòu)造方法 |
2 | public Thread(Runnable target) | 傳入實現(xiàn)Runnable接口的對象(任務(wù)對象)構(gòu)造線程 |
3 | public Thread(Runnable target, String name) | 根據(jù)目標(biāo)任務(wù)并指定線程名創(chuàng)建線程 |
4 | public Thread(ThreadGroup group, Runnable target) | 根據(jù)線程組和任務(wù)創(chuàng)建線程(了解) |
5 | public Thread(ThreadGroup group, Runnable target, String name) | 比構(gòu)造方法4多一個指定線程名 |
6 | public Thread(String name) | 指定線程名創(chuàng)建線程 |
7 | public Thread(ThreadGroup group, String name) | 根據(jù)線程組并指定線程名創(chuàng)建線程 |
8 | public Thread(ThreadGroup group, Runnable target, String name,long stackSize) | 構(gòu)造函數(shù)與構(gòu)造方法5相同,只是它允許指定線程堆棧大小 |
注:線程可以被用來分組管理,分好的組即為線程組,Runnable
類表示任務(wù)類,也就是線程需執(zhí)行的任務(wù)。
1.1.3啟用java線程必會的方法
想要使用java線程至少得知道Thread類中這幾個方法:
方法名 | 解釋 |
---|---|
public void run() | 該方法用來封裝線程運行時執(zhí)行的內(nèi)容 |
public synchronized void start() | 線程創(chuàng)建并執(zhí)行run方法 |
public static native void sleep(long millis) throws InterruptedException | 使線程休眠millis毫秒 |
創(chuàng)建Thread
對象,必須重寫run
方法,因為你創(chuàng)建一個線程肯定要用運行一些代碼嘛。
1.2第一個Java多線程程序
首先,我們可以創(chuàng)建一個MyThread
類繼承Thread
類,并重寫run
方法。
class MyThread extends Thread{ //重寫run方法 @Override public void run() { System.out.println("你好!線程!"); } } public class TestDemo { public static void main(String[] args) { //創(chuàng)建MyThread線程對象,但是線程沒有創(chuàng)建 Thread thread = new MyThread(); //線程創(chuàng)建并運行 thread.start(); } }
使用new
創(chuàng)建線程對象,線程并沒有被創(chuàng)建,僅僅只是單純地創(chuàng)建了一個線程對象,運行start
方法時才會創(chuàng)建線程并執(zhí)行run
方法。
運行結(jié)果:
1.3使用Runnable對象創(chuàng)建線程
除了使用子類繼承Thread
類并重寫run
方法,使用子類實現(xiàn)Runnable
接口(該接口中也有一個run
方法,表示任務(wù)的內(nèi)容),該對象可以理解為“任務(wù)”,也就是說Thread
對象可以接受Runnable
引用,并執(zhí)行Runnable
引用的run
方法。
因為Runable
是一個接口,所以需要實現(xiàn)run
方法,線程Thread
對象創(chuàng)建好后,此時線程并沒有創(chuàng)建運行,需要調(diào)用start
方法來創(chuàng)建啟動線程。
class MyRunnable implements Runnable { @Override public void run() { System.out.println("使用Runnable描述任務(wù)!"); } } public class TestDemo3 { public static void main(String[] args) { //將Runnable任務(wù)傳給Thread對象來創(chuàng)建運行線程 Runnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
運行結(jié)果:
根據(jù)“低內(nèi)聚,高耦合”的
編程風(fēng)格,使用Runnable
的方式創(chuàng)建更優(yōu)。
1.4使用內(nèi)部類創(chuàng)建線程
當(dāng)然也可以使用匿名內(nèi)部類,來傳入匿名對象來重寫run
方法。
public class TestDemo4 { public static void main(String[] args) { Thread thread = new Thread() { @Override public void run() { System.out.println("使用匿名內(nèi)部類創(chuàng)建線程匿名對象"); } }; thread.start(); } }
運行結(jié)果:
1.5使用Lambda表達(dá)式創(chuàng)建線程
使用Lambda表達(dá)式,本質(zhì)還是使用匿名內(nèi)部類創(chuàng)建的Thread
。
public class TestDemo6 { public static void main(String[] args) { Thread thread = new Thread(() -> System.out.println("使用Lambda表達(dá)式表示匿名內(nèi)部類來創(chuàng)建匿名任務(wù)")); thread.start(); } }
運行結(jié)果:
1.6多線程并發(fā)執(zhí)行簡單演示
在一個進(jìn)程中至少會有一個線程,如果不使用多線程編程,一個進(jìn)程中默認(rèn)會有執(zhí)行main
方法的main
線程(該線程是自動創(chuàng)建的),當(dāng)你創(chuàng)建一個新的線程t
,該線程會與main
線程并發(fā)執(zhí)行。
public class TestDemo7 { public static void main(String[] args) { //thread 線程 Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("thread線程執(zhí)行中!"); //為了使效果更加明顯 可以使用sleep方法設(shè)定線程睡眠時間 try { Thread.sleep(1000);//每執(zhí)行一次循環(huán)就睡眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.start(); //main 線程 for (int i = 0; i < 10; i++) { System.out.println("main線程執(zhí)行中!"); //為了使效果更加明顯 可以使用sleep方法設(shè)定線程睡眠時間 try { Thread.sleep(1000);//每執(zhí)行一次循環(huán)就睡眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } }
運行結(jié)果:
從上面的運行結(jié)果可以看出一個問題,因為thread
線程與main
線程都是每打印一句語句線程休眠1
秒,兩個線程喚醒的先后順序是隨機(jī)的,這也是java多線程中的一個“萬惡之源”,這個問題給我們帶來了很多麻煩,細(xì)節(jié)等后續(xù)的博客細(xì)說。
1.7多線程并發(fā)執(zhí)行的優(yōu)勢
加入我們現(xiàn)在有一個任務(wù),就是分別將a
和b
兩個變量都自增20億次,我們來看看使用兩個線程和單獨使用一個線程分別所需的時間是多少。
public class Test { private static final long COUNT = 20_0000_0000L; //兩個線程 public static void many() throws InterruptedException { //獲取開始執(zhí)行時間戳 long start = System.currentTimeMillis(); Thread thread1 = new Thread(() -> { long a = 0; for (long i = 0; i < COUNT; i++) { a++; } }); thread1.start(); Thread thread2 = new Thread(() -> { long b = 0; for (long i = 0; i < COUNT; i++) { b++; } }); thread2.start(); //等待兩個線程結(jié)束 再獲取結(jié)束時的時間戳 thread1.join(); thread2.join(); long end = System.currentTimeMillis(); //執(zhí)行時間,單位為毫秒 System.out.println("多線程執(zhí)行時間:" + (end - start) + "ms"); } //單線程 public static void single() { //記錄開始執(zhí)行的時間戳 long start = System.currentTimeMillis(); long a = 0; for (long i = 0; i < COUNT; i++) { a++; } long b = 0; for (long i = 0; i < COUNT; i++) { b++; } //獲取執(zhí)行結(jié)束時的時間戳 long end = System.currentTimeMillis(); System.out.println("單線程執(zhí)行時間:" + (end - start) + "ms"); } public static void main(String[] args) throws InterruptedException { //多線程 many(); //單線程 single(); } }
我們來看看完成這個任務(wù)所需的時間:
根據(jù)結(jié)果我們發(fā)現(xiàn)兩個線程并發(fā)執(zhí)行的時間大約是500ms
左右,單線程執(zhí)行的時間大約是1000ms
左右,當(dāng)然如果任務(wù)量不夠大,可能多線程相比于單線程并不會有優(yōu)勢,畢竟創(chuàng)建線程本身還是有開銷的。
2.Thread類的常用屬性與方法
2.1Thread類中的重要屬性
屬性 | 獲取該屬性的方法 |
---|---|
線程的唯一標(biāo)識ID | public long getId() |
線程的名稱name | public final String getName() |
線程的狀態(tài)state | public State getState() |
線程的優(yōu)先級priority | public final int getPriority() |
線程是否后臺線程 | public final boolean isDaemon() |
線程是否存活 | public final native boolean isAlive() |
線程是否中斷 | public boolean isInterrupted() |
每一個線程都擁有一個id
作為標(biāo)識,其中處于同一進(jìn)程的所有線程id
相同,每個進(jìn)程間都有唯一的id
標(biāo)識。
線程也是擁有名字的,如果我們創(chuàng)建Thread
對象時,沒有指定線程對象的名稱,則會默認(rèn)命名為Thread-i
,其中i
為整數(shù)。
通過了解進(jìn)程,我們知道進(jìn)程擁有3
種狀態(tài),分別為阻塞,執(zhí)行和就緒。而java中的線程也有類似與這種狀態(tài)的定義,后面我們細(xì)說,優(yōu)先級也一樣就不用多說了。
java線程分為后臺線程與前臺線程,其中后臺線程不會影響到進(jìn)程的退出,而前臺線程會影響進(jìn)程的退出,比如有線程t1
與線程t2
,當(dāng)這兩個線程為前臺線程時,main
方法執(zhí)行完畢時,t1
與t2
不會立即退出,要等到線程執(zhí)行完畢,整個進(jìn)程才會退出,反之,當(dāng)這兩個線程為后臺線程時,main
方法執(zhí)行完畢時,t1
與t2
線程被強(qiáng)制結(jié)束,整個進(jìn)程也就結(jié)束了。
關(guān)于java線程的屬性,我們可以通過java官方的jconsole
調(diào)試工具查看java線程的一些屬性。 這個工具一般在jdk的bin
目錄,
雙擊打開有如下界面:
選擇需要查看的線程并查看:
選擇你需要查看的進(jìn)程屬性:
2.2Thread類中常用方法總結(jié)
2.2.1常用方法
方法名 | 解釋 |
---|---|
public void run() | 該方法用來封裝線程運行時執(zhí)行的內(nèi)容 |
public synchronized void start() | 線程創(chuàng)建并執(zhí)行run方法 |
public static native void sleep(long millis) throws InterruptedException | 使線程休眠millis毫秒 |
public final void join() throws InterruptedException | 等待線程結(jié)束(在哪個線程中調(diào)用哪個對象的join方法,哪個線程就等待哪個對象) |
public final synchronized void join(long millis) throws InterruptedException | 等待線程結(jié)束,最多等待millis毫秒 |
public final synchronized void join(long millis, int nanos) throws InterruptedException | 指定最多等待時間等待線程,精確到納秒 |
public void interrupt() | 中斷線程對象所關(guān)聯(lián)的對象,如果線程在休眠(阻塞狀態(tài))會拋出異常通知,否則設(shè)置中斷標(biāo)志位 |
public static boolean interrupted() | 判斷當(dāng)前線程的中斷標(biāo)志位是否設(shè)置,調(diào)用后會清除線程的中斷標(biāo)志位 |
public boolean isInterrupted() | 判斷當(dāng)前線程的中斷標(biāo)志位是否設(shè)置,調(diào)用后不會影響線程的標(biāo)志位 |
public final synchronized void setName(String name) | 修改線程對象名稱 |
public static native Thread currentThread() | 獲取當(dāng)前線程對象 |
2.2.2中斷線程
如果我們想中斷一個正在執(zhí)行的線程,該如何做呢?最簡單但不嚴(yán)謹(jǐn)?shù)姆椒ň褪俏覀冊?code>run方法中定義一個中斷標(biāo)志位(需要中斷時標(biāo)志位為true
,默認(rèn)情況為false
),每次執(zhí)行具體任務(wù)時需要先判斷中斷標(biāo)志位是否為true
,如果是就結(jié)束線程,否則繼續(xù)執(zhí)行。
public class TestDemo8 { private static boolean isQuit = false; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while(!isQuit) { //每隔1秒打印一句 System.out.println("一個不起眼的線程!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); //main線程阻塞5秒 按理會打印5句話 Thread.sleep(5000); isQuit = true; } }
運行結(jié)果:
但是該方法是不夠嚴(yán)謹(jǐn)?shù)?,有些場景可能達(dá)不到預(yù)期的效果,最優(yōu)的做法就是調(diào)整線程對象或者線程類中的自帶標(biāo)志位。
方式1:使用Thread對象中的標(biāo)志位首先使用 currentThread
方法獲取線程對象,然后再調(diào)用該對象中的isterrupted
方法獲取該對象的中斷標(biāo)志位代替我們自己所寫的isQuit
標(biāo)志位,然后等該線程運行一段時間后使用interrupt
方法改變標(biāo)志位,中斷線程,寫出如下代碼,看看能不能達(dá)到預(yù)期效果:
public class TestDemo9 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() ->{ while (!Thread.currentThread().isInterrupted()) { System.out.println("又是一個不起眼的線程!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); //main休眠5秒 Thread.sleep(5000); //使用interrupt方法修改線程標(biāo)志位,使其中斷 thread.interrupt(); } }
我們來看一看:
失敗了,拋出一個InterruptedException
異常后,線程沒有中斷,而是繼續(xù)運行,原因是interrupt
方法遇到因為調(diào)用 wait/join/sleep
等方法而阻塞的線程時會使sleep
等方法拋出異常,并且中斷標(biāo)志位不會修改為true
,這時我們的catch
語句里面值輸出了異常信息并沒有去中斷異常,所以我們需要在catch
語句中加上線程結(jié)束的收尾工作代碼和退出任務(wù)循環(huán)的break
語句就可以了。
public class TestDemo9 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() ->{ while (!Thread.currentThread().isInterrupted()) { System.out.println("又是一個不起眼的線程!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); //收尾工作 System.out.println("收尾工作!"); break; } } }); thread.start(); //main休眠5秒 Thread.sleep(5000); //使用interrupt方法修改線程標(biāo)志位,使其中斷 thread.interrupt(); } }
運行結(jié)果:
方式2:使用Thread類中的標(biāo)志位除了isInterrupted
,還有一個靜態(tài)方法interrupted
能夠訪問類中的標(biāo)志位,一般一個程序中只有一個,我們也可以使用該靜態(tài)方法來作為中斷標(biāo)志位,然后到時機(jī)后使用interrupt
方法來中斷線程執(zhí)行。
public class TestDemo10 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (!Thread.interrupted()) { System.out.println("又又是一個不起眼的線程!"); try { //設(shè)置打印頻率為1s Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); //收尾工作 System.out.println("收尾工作!"); break; } } }); thread.start(); //main休眠5秒 Thread.sleep(5000); //使用interrupt方法修改線程標(biāo)志位,使其中斷 thread.interrupt(); } }
運行結(jié)果:
綜上所述,一般以方式1的方式無腦中斷線程就可以。
2.2.3線程等待
像上面的計算自增20億次的例子就需要線程等待join
方法,main
線程需要等兩個線程運行完畢后才能計算計算結(jié)束時的時間戳。
針對這一點java還準(zhǔn)備了帶參數(shù)的join
方法,可以指定最長的等待時間。
還有一個細(xì)節(jié)那join
方法是誰等誰呢?
我們來假設(shè)幾個線程,線程A表示調(diào)用join
方法的線程,線程B表示join
方法來自B線程對象,那么在A線程使用B.join
方法,那就是A線程等待B線程結(jié)束。
2.2.4start方法與run方法的區(qū)別
我們知道執(zhí)行一個線程的任務(wù)就是線程對象中所重寫的run
方法,那么可以直接調(diào)用run
方法來代替start
方法嗎?
當(dāng)然不行!因為你調(diào)用run
方法就是單純地調(diào)用了Thread對象中的一個普通方法而已,并沒有創(chuàng)建一個新線程來執(zhí)行run
方法,而是通過main
線程來執(zhí)行的run
方法,而使用start
方法,會創(chuàng)建一個新線程并執(zhí)行run
方法。
3.Java線程的狀態(tài)
3.1java線程中的基本狀態(tài)
操作系統(tǒng)中進(jìn)程的狀態(tài)有三種分別為阻塞,就緒和執(zhí)行,而java線程中的狀態(tài)基本上相同,但做了細(xì)分,有一點區(qū)別,我們來認(rèn)識一下。
NEW
: 安排了工作, 還未開始行動,就是線程對象存在,但沒有執(zhí)行start
方法,java內(nèi)部的狀態(tài),與進(jìn)程中的狀態(tài)無關(guān)。RUNNABLE
: 就緒狀態(tài)。BLOCKED
: 線程正在等待鎖釋放而引起的阻塞狀態(tài)(synchronized加鎖)。WAITING
: 線程正在等待等待喚醒而引起的阻塞狀態(tài)(waitf方法使線程等待喚醒)。TIMED_WAITING
: 在一段時間內(nèi)處于阻塞狀態(tài),通常是使用sleep
或者join(帶參數(shù))
方法引起。TERMINATED
:Thread對象還存在,但是關(guān)聯(lián)的線程已經(jīng)工作完成了,java內(nèi)部的狀態(tài),與進(jìn)程中的狀態(tài)無關(guān)。
3.2線程狀態(tài)轉(zhuǎn)移
我先使用一個流程圖來簡要說明狀態(tài)之間的關(guān)系:
上面這個圖簡單地說明了這幾種狀態(tài)之間的轉(zhuǎn)移,關(guān)于圖中的wait
以及synchronized
關(guān)鍵字會在討論線程安全問題時介紹。
這期的內(nèi)容分享了有關(guān)線程創(chuàng)建執(zhí)行以及有關(guān)Thread類中的基本方法,下期繼續(xù)介紹多線程更深入的知識,比如線程安全問題,如何加鎖等更深一點的內(nèi)容。
到此這篇關(guān)于Java線程創(chuàng)建與Thread類的使用方法的文章就介紹到這了,更多相關(guān)Java線程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決異常FileNotFoundException:class path resource找不到資源文件的問題
今天小編就為大家分享一篇關(guān)于解決異常FileNotFoundException:class path resource找不到資源文件的問題,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12手把手教你如何用JAVA連接MYSQL(mysql-connector-j-8.0.32.jar)
這篇文章主要介紹了關(guān)于如何用JAVA連接MYSQL(mysql-connector-j-8.0.32.jar)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用MySQL具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01Java 動態(tài)數(shù)組的實現(xiàn)示例
Java動態(tài)數(shù)組是一種可以任意伸縮數(shù)組長度的對象,本文主要介紹了Java 動態(tài)數(shù)組的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08Springboot通過lucene實現(xiàn)全文檢索詳解流程
Lucene是一個基于Java的全文信息檢索工具包,它不是一個完整的搜索應(yīng)用程序,而是為你的應(yīng)用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta 家族中的一個開源項目,也是目前最為流行的基于 Java 開源全文檢索工具包2022-06-06