入門Java線程基礎(chǔ)一篇就夠了
簡介:
線程是操作系統(tǒng)調(diào)度的最小單元,在多核環(huán)境中,多個(gè)線程能同時(shí)執(zhí)行,如果運(yùn)用得當(dāng),能顯著的提升程序的性能。
一、線程初步認(rèn)識(shí)
1、什么是線程
操作系統(tǒng)運(yùn)行一個(gè)程序會(huì)為其啟動(dòng)一個(gè)進(jìn)程。例如,啟動(dòng)一個(gè)Java程序會(huì)創(chuàng)建一個(gè)Java進(jìn)程。現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,線程也稱為輕量級(jí)進(jìn)程(Light Weight Process),一個(gè)進(jìn)程中可以創(chuàng)建一個(gè)到多個(gè)線程,線程擁有自己的計(jì)數(shù)器、堆棧和局部變量等屬性,并且能訪問共享的內(nèi)存變量。處理器會(huì)通過快速切換這些線程,來執(zhí)行程序。
2、Java本身就是多線程
示例代碼:
package com.lizba.p2; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.util.Arrays; /** * <p> * * </p> * * @Author: Liziba * @Date: 2021/6/13 23:03 */ public class MultiThread { public static void main(String[] args) { // 獲取Java線程管理MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 獲取線程和線程堆棧信息; // boolean lockedMonitors = false 不需要獲取同步的monitor信息; // boolean lockedSynchronizers = false 不需要獲取同步的synchronizer信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 打印線程ID和線程name Arrays.stream(threadInfos).forEach(threadInfo -> { System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()); }); } }
輸出結(jié)果(不一定一致):
[6]Monitor Ctrl-Break // idea中特有的線程(不用管)
[5]Attach Listener // JVM進(jìn)程間的通信線程
[4]Signal Dispatcher // 分發(fā)處理發(fā)送給JVM信號(hào)的線程
[3]Finalizer // 調(diào)用對象的finalizer線程
[2]Reference Handler // 清楚Reference的線程
[1]main // main線程,用戶程序入口
總結(jié):從輸出結(jié)果不難看出,Java程序本身就是多線程的。它不僅僅只有一個(gè)main線程在運(yùn)行,而是main線程和其他多個(gè)線程在同時(shí)運(yùn)行。
3、為什么要使用多線程
使用多線程的好處如下:
1.更多處理器核心
計(jì)算機(jī)處理器核心數(shù)增多,由以前的高主頻向多核心技術(shù)發(fā)展,現(xiàn)在的計(jì)算機(jī)更擅長于并行計(jì)算,因此如何充分利用多核心處理器是現(xiàn)在的主要問題。線程是操作系統(tǒng)調(diào)度的最小單元,一個(gè)程序作為一個(gè)進(jìn)程來運(yùn)行,它會(huì)創(chuàng)建多個(gè)線程,而一個(gè)線程在同一時(shí)刻只能運(yùn)行在一個(gè)處理器上。因此一個(gè)進(jìn)程如果能使用多線程計(jì)算,將其計(jì)算邏輯分配到多個(gè)處理器核心上,那么相比單線程運(yùn)行將會(huì)有更顯著的性能提升。
2.更快響應(yīng)時(shí)間
在復(fù)雜業(yè)務(wù)場景中,我們可以將非強(qiáng)一致性關(guān)聯(lián)的業(yè)務(wù)派發(fā)給其他線程處理(或者使用消息隊(duì)列)。這樣可以減少應(yīng)用響應(yīng)用戶請求的時(shí)間
3.更好的編程模型
合理使用Java的提供的多線程編程模型,能使得程序員更好的解決問題,而不需要過于復(fù)雜的考慮如何將其多線程化。
4、線程的優(yōu)先級(jí)
現(xiàn)代操作系統(tǒng)基本采用的是時(shí)間片分配的方式來調(diào)度線程,也就是操作系統(tǒng)將CPU的運(yùn)行分為一個(gè)個(gè)時(shí)間片,線程會(huì)分配的若干時(shí)間片,當(dāng)線程時(shí)間片用完了,就會(huì)發(fā)生線程調(diào)度等待下次時(shí)間片的分配。線程在一次CPU調(diào)度中能執(zhí)行多久,取決于所分時(shí)間片的多少,而線程優(yōu)先級(jí)就是決定線程需要多或者少分配一些處理器資源的線程屬性。在Java線程中,線程的優(yōu)先級(jí)的可設(shè)置范圍是1-10,默認(rèn)優(yōu)先級(jí)是5,理論上優(yōu)先級(jí)高的線程分配時(shí)間片數(shù)量要優(yōu)先于低的線程(部分操作系統(tǒng)這個(gè)設(shè)置是不生效的);
示例代碼:
package com.lizba.p2; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * <p> * 線程優(yōu)先級(jí)設(shè)置 * </p> * * @Author: Liziba * @Date: 2021/6/14 12:03 */ public class Priority { /** 線程執(zhí)行流程控制開關(guān) */ private static volatile boolean notStart = true; /** 線程執(zhí)行流程控制開關(guān) */ private static volatile boolean notEnd = true; public static void main(String[] args) throws InterruptedException { List<Job> jobs = new ArrayList<>(); // 設(shè)置5個(gè)優(yōu)先級(jí)為1的線程,設(shè)置5個(gè)優(yōu)先級(jí)為10的線程 for (int i = 0; i < 10; i++) { int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY; Job job = new Job(priority); jobs.add(job); Thread thread = new Thread(job, "Thread:" + i); thread.setPriority(priority); thread.start(); } notStart = false; TimeUnit.SECONDS.sleep(10); notEnd = false; jobs.forEach( job -> System.out.println("Job priority : " + job.priority + ", Count : " + job.jobCount) ); } /** * 通過Job來記錄線程的執(zhí)行次數(shù)和優(yōu)先級(jí) */ static class Job implements Runnable { private int priority; private long jobCount; public Job(int priority) { this.priority = priority; } @Override public void run() { while (notStart) { // 讓出CPU時(shí)間片,等待下次調(diào)度 Thread.yield(); } while (notEnd) { // 讓出CPU時(shí)間片,等待下次調(diào)度 Thread.yield(); jobCount++; } } } }
執(zhí)行結(jié)果:
從輸出結(jié)果上來看,優(yōu)先級(jí)為1的線程和優(yōu)先級(jí)為10的線程執(zhí)行的次數(shù)非常相近,因此這表明程序正確性是不能依賴線程的優(yōu)先級(jí)高低的。
5、線程的狀態(tài)
線程的生命周期如下:
狀態(tài)名稱 | 說明 |
---|---|
NEW | 初始狀態(tài),線程被構(gòu)建,并未調(diào)用start()方法 |
RUNNABLE | 運(yùn)行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)統(tǒng)稱為“運(yùn)行中” |
BLOCKED | 阻塞狀態(tài),線程阻塞于鎖 |
WAITING | 等待狀態(tài),線程進(jìn)入等待狀態(tài),進(jìn)入該狀態(tài)表示當(dāng)前線程需要等待其他線程作出一些特定動(dòng)作(通知或中斷) |
TIME_WAITING | 超時(shí)等待,先比WAITING可以在指定的時(shí)間內(nèi)自行返回 |
TERMINATED | 終止?fàn)顟B(tài),表示當(dāng)前線程已經(jīng)執(zhí)行完畢 |
通過代碼來查看Java線程的狀態(tài)
代碼示例:
package com.lizba.p2; import java.util.concurrent.TimeUnit; /** * <p> * 睡眠指定時(shí)間工工具類 * </p> * * @Author: Liziba * @Date: 2021/6/14 13:27 */ public class SleepUtil { public static final void sleepSecond(long seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } } }
package com.lizba.p2; /** * <p> * 線程狀態(tài)示例代碼 * </p> * * @Author: Liziba * @Date: 2021/6/14 13:25 */ public class ThreadStateDemo { public static void main(String[] args) { // TimeWaiting new Thread(new TimeWaiting(), "TimeWaitingThread").start(); // Waiting new Thread(new Waiting(), "WaitingThread").start(); // Blocked1和Blocked2一個(gè)獲取鎖成功,一個(gè)獲取失敗 new Thread(new Blocked(), "Blocked1Thread").start(); new Thread(new Blocked(), "Blocked2Thread").start(); } // 線程不斷的進(jìn)行睡眠 static class TimeWaiting implements Runnable { @Override public void run() { while (true) { SleepUtil.sleepSecond(100); } } } // 線程等待在Waiting.class實(shí)例上 static class Waiting implements Runnable { @Override public void run() { while (true) { synchronized (Waiting.class) { try { Waiting.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } // 該線程Blocked.class實(shí)例上加鎖,不會(huì)釋放該鎖 static class Blocked implements Runnable { @Override public void run() { synchronized (Blocked.class) { while (true) { SleepUtil.sleepSecond(100); } } } } }
使用JPS查看Java進(jìn)程:
查看示例代碼ThreadStateDemo進(jìn)程ID是2576,鍵入jstack 2576查看輸出:
整理輸出結(jié)果:
線程名稱 | 線程狀態(tài) |
---|---|
Blocked2Thread | BLOCKED (on object monitor),阻塞在獲取Blocked.class的鎖上 |
Blocked1Thread | TIMED_WAITING (sleeping) |
WaitingThread | WAITING (on object monitor) |
TimeWaitingThread | TIMED_WAITING (sleeping) |
總結(jié):線程在自身生命周期中不是規(guī)定處于某一個(gè)狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換。
Java線程的狀態(tài)變化圖如下:
Java線程狀態(tài)變遷圖
總結(jié):
- 線程創(chuàng)建后,調(diào)用start()方法開始運(yùn)行
- 線程執(zhí)行wait()方法后,線程進(jìn)入等待狀態(tài),進(jìn)入等待的線程需要依靠其他線程才能夠返回到運(yùn)行狀態(tài)
- 超時(shí)等待相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,達(dá)到設(shè)置的超時(shí)時(shí)間后返回到運(yùn)行狀態(tài)
- 線程執(zhí)行同步方法或代碼塊時(shí),未獲取到鎖的線程,將會(huì)進(jìn)入到阻塞狀態(tài)。
- 線程執(zhí)行完Runnable的run()方法之后進(jìn)入到終止?fàn)顟B(tài)
- 阻塞在Java的concurrent包中Lock接口的線程是等待狀態(tài),因?yàn)長ock接口阻塞的實(shí)現(xiàn)使用的是Daemon線程
6、Daemon線程
簡介:
Daemon線程是一種支持型線程,它的主要作用是程序中后臺(tái)調(diào)度和支持性工作。當(dāng)一個(gè)Java虛擬機(jī)中不存在非Daemon線程的時(shí)候,Java虛擬機(jī)將會(huì)退出。Daemon線程需要在啟動(dòng)之前設(shè)置,不能在啟動(dòng)之后設(shè)置。
設(shè)置方式:
Thread.setDaemon(true)
需要特別注意的點(diǎn):
Daemon線程被用作支持性工作的完成,但是在Java虛擬機(jī)退出時(shí)Daemon線程的finally代碼塊不一定執(zhí)行。
示例代碼:
package com.lizba.p2; /** * <p> * DaemonRunner線程 * </p> * * @Author: Liziba * @Date: 2021/6/14 19:50 */ public class DaemonRunner implements Runnable{ @Override public void run() { try { SleepUtil.sleepSecond(100); } finally { System.out.println("DaemonRunner finally run ..."); } } }
測試:
package com.lizba.p2; /** * <p> * * </p> * * @Author: Liziba * @Date: 2021/6/14 19:59 */ public class DaemonTest { public static void main(String[] args) { Thread t = new Thread(new DaemonRunner(), "DaemonRunner"); t.setDaemon(true); t.start(); } }
輸出結(jié)果:
總結(jié):
不難發(fā)現(xiàn),DaemonRunner的run方法的finally代碼塊并沒有執(zhí)行,這是因?yàn)?,?dāng)Java虛擬機(jī)中已經(jīng)沒有非Daemon線程時(shí),虛擬機(jī)會(huì)立即退出,虛擬機(jī)中的所以daemon線程需要立即終止,所以線程DaemonRunner會(huì)被立即終止,finally并未執(zhí)行。
二、線程啟動(dòng)和終止
1、構(gòu)造線程
運(yùn)行線程之前需要構(gòu)造一個(gè)線程對象,線程對象在構(gòu)造的時(shí)候需要設(shè)置一些線程的屬性,這些屬性包括線程組、線程的優(yōu)先級(jí)、是否是daemon線程、線程名稱等信息。
代碼示例:
來自java.lang.Thread
/** * Initializes a Thread. * * @param g the Thread group * @param target the object whose run() method gets called * @param name the name of the new Thread * @param stackSize the desired stack size for the new thread, or * zero to indicate that this parameter is to be ignored. * @param acc the AccessControlContext to inherit, or * AccessController.getContext() if null * @param inheritThreadLocals if {@code true}, inherit initial values for * inheritable thread-locals from the constructing thread */ private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); }// 設(shè)置線程名稱 this.name = name;// 當(dāng)前線程設(shè)置為該線程的父線程 Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted();// 設(shè)置線程組 this.group = g; // 將daemon屬性設(shè)置為父線程的對應(yīng)的屬性 this.daemon = parent.isDaemon(); // 將prority屬性設(shè)置為父線程的對應(yīng)的屬性 this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); // 復(fù)制父線程的InheritableThreadLocals屬性 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; // 設(shè)置一個(gè)線程id tid = nextThreadID(); }
總結(jié):
在上述代碼中,一個(gè)新構(gòu)建的線程對象時(shí)由其parent線程來分配空間的,而child繼承了parent是否為Daemon、優(yōu)先級(jí)和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時(shí)會(huì)分配一個(gè)唯一的ID來標(biāo)志線程。此時(shí)一個(gè)完整的能夠運(yùn)行的線程對象就初始化好了,在堆內(nèi)存中等待運(yùn)行。
2、什么是線程中斷
中斷可以理解為線程的一個(gè)標(biāo)識(shí)位屬性,它表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。線程通過檢查自身是否被中斷來進(jìn)行響應(yīng),線程通過方法isInterrupted()來進(jìn)行判斷是否被中斷,也可以通過調(diào)用靜態(tài)方法Thread.interrupted()對當(dāng)前線程的中斷標(biāo)志位進(jìn)行復(fù)位。如下情況不能準(zhǔn)確判斷線程是否被中斷過:
線程已經(jīng)終止運(yùn)行,即使被中斷過,isInterrupted()方法也會(huì)返回false方法拋出InterruptedException異常,即使被中斷過,調(diào)用isInterrupted()方法將會(huì)返回false,這是因?yàn)閽伋鯥nterruptedException之前會(huì)清除中斷標(biāo)志。
示例代碼:
package com.lizba.p2; /** * <p> * 線程中斷示例代碼 * </p> * * @Author: Liziba * @Date: 2021/6/14 20:36 */ public class Interrupted { public static void main(String[] args) { // sleepThread不停的嘗試睡眠 Thread sleepThread = new Thread(new SleepRunner(), "sleepThread"); sleepThread.setDaemon(true); // busyThread Thread busyThread = new Thread(new BusyRunner(), "busyThread"); busyThread.setDaemon(true); // 啟動(dòng)兩個(gè)線程 sleepThread.start(); busyThread.start(); // 休眠5秒,讓sleepThread和busyThread運(yùn)行充分 SleepUtil.sleepSecond(5); // 中斷兩個(gè)線程 sleepThread.interrupt(); busyThread.interrupt(); System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted()); System.out.println("BusyThread interrupted is " + busyThread.isInterrupted()); // 睡眠主線程,防止daemon線程退出 SleepUtil.sleepSecond(2); } static class SleepRunner implements Runnable { @Override public void run() { while (true) { SleepUtil.sleepSecond(10); } } } static class BusyRunner implements Runnable { @Override public void run() { while (true) {} } } }
查看運(yùn)行結(jié)果:
總結(jié):
拋出InterruptedException的是sleepThread線程,雖然兩者都被中斷過,但是sleepThread線程的中斷標(biāo)志返回的是false,這是因?yàn)門imeUnit.SECONDS.sleep(seconds)會(huì)拋出InterruptedException異常,拋出異常之前,sleepThread線程的中斷標(biāo)志被清除了。但是,busyThread一直在運(yùn)行沒有拋出異常,中斷位沒有被清除。
3、suspend()、resume()和stop()
舉例:
線程這三個(gè)方法,相當(dāng)于QQ音樂播放音樂時(shí)的暫停、恢復(fù)和停止操作。(注意這些方法已經(jīng)過期了,不建議使用。)
示例代碼:
package com.lizba.p2; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; /** * <p> * 線程過期方法示例 * </p> * * @Author: Liziba * @Date: 2021/6/14 20:57 */ public class Deprecated { static DateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) { Thread printThread = new Thread(new PrintThread(), "PrintThread"); printThread.start(); SleepUtil.sleepSecond(3); // 暫停printThread輸出 printThread.suspend(); System.out.println("main suspend PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); // 恢復(fù)printThread輸出 printThread.resume(); System.out.println("main resume PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); // 終止printThread輸出 printThread.stop(); System.out.println("main stop PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); } static class PrintThread implements Runnable { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + "Run at " + format.format(new Date())); SleepUtil.sleepSecond(1); } } } }
輸出結(jié)果:
總結(jié):
上述代碼執(zhí)行輸出的結(jié)果,與API說明和我們的預(yù)期完成一致,但是看似正確的代碼卻隱藏這很多問題。
存在問題:
- suspend()方法調(diào)用后不會(huì)釋放已占有的資源(比如鎖),可能會(huì)導(dǎo)致死鎖
- stop()方法在終結(jié)一個(gè)線程時(shí)不能保證資源的正常釋放,可能會(huì)導(dǎo)致程序處于不確定的工作狀態(tài)
4、正確的終止線程
調(diào)用線程的interrupt()方法使用一個(gè)Boolean類型的變量來控制是否停止任務(wù)并終止線程
示例代碼:
package com.lizba.p2; /** * <p> * 標(biāo)志位終止線程示例代碼 * </p> * * @Author: Liziba * @Date: 2021/6/14 21:17 */ public class ShutDown { public static void main(String[] args) { Runner one = new Runner(); Thread t = new Thread(one, "CountThread"); t.start(); SleepUtil.sleepSecond(1); t.interrupt(); Runner two = new Runner(); t = new Thread(two, "CountThread"); t.start(); SleepUtil.sleepSecond(1); two.cancel(); } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " +i); } /** * 關(guān)閉 */ public void cancel() { on = false; } } }
輸出結(jié)果:
總結(jié):
main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這兩種方法終止線程的好處是能讓線程在終止時(shí)有機(jī)會(huì)去清理資源。做法更加安全和優(yōu)雅。希望大家可以多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java如何通過"枚舉的枚舉"表示二級(jí)分類的業(yè)務(wù)場景
這篇文章主要介紹了Java如何通過"枚舉的枚舉"表示二級(jí)分類的業(yè)務(wù)場景問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06SpringBoot實(shí)現(xiàn)過濾器和攔截器的方法
大家應(yīng)該都曉得實(shí)現(xiàn)過濾器需要實(shí)現(xiàn)?javax.servlet.Filter?接口,而攔截器會(huì)在處理指定請求之前和之后進(jìn)行相關(guān)操作,配置攔截器需要兩步,本文通過實(shí)例代碼給大家介紹SpringBoot?過濾器和攔截器的相關(guān)知識(shí),感興趣的朋友一起看看吧2022-11-11使用Spring boot + jQuery上傳文件(kotlin)功能實(shí)例詳解
本文通過實(shí)例代碼給大家介紹了使用Spring boot + jQuery上傳文件(kotlin) 功能,需要的朋友可以參考下2017-07-07websocket在springboot+vue中的使用教程
這篇文章主要介紹了websocket在springboot+vue中的使用教程,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08java實(shí)現(xiàn)多設(shè)備同時(shí)登錄或強(qiáng)制下線
本文主要介紹了java實(shí)現(xiàn)多設(shè)備同時(shí)登錄或強(qiáng)制下線,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07SpringBoot啟動(dòng)嵌入式Tomcat的實(shí)現(xiàn)步驟
本文主要介紹了淺談SpringBoot如何啟動(dòng)嵌入式Tomcat,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08詳解java創(chuàng)建一個(gè)女朋友類(對象啥的new一個(gè)就是)==建造者模式,一鍵重寫
這篇文章主要介紹了java建造者模式一鍵重寫,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04