java線程的基礎實例解析
一、線程初步認識
1、什么是線程
操作系統(tǒng)運行一個程序會為其啟動一個進程。例如,啟動一個Java程序會創(chuàng)建一個Java進程?,F(xiàn)代操作系統(tǒng)調度的最小單元是線程,線程也稱為輕量級進程(Light Weight Process),一個進程中可以創(chuàng)建一個到多個線程,線程擁有自己的計數(shù)器、堆棧和局部變量等屬性,并且能訪問共享的內存變量。處理器會通過快速切換這些線程,來執(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;
/**
*
* @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());
});
}
}
輸出結果(不一定一致):
[6]Monitor Ctrl-Break // idea中特有的線程(不用管)
[5]Attach Listener // JVM進程間的通信線程
[4]Signal Dispatcher // 分發(fā)處理發(fā)送給JVM信號的線程
[3]Finalizer // 調用對象的finalizer線程
[2]Reference Handler // 清楚Reference的線程
[1]main // main線程,用戶程序入口
總結:從輸出結果不難看出,Java程序本身就是多線程的。它不僅僅只有一個main線程在運行,而是main線程和其他多個線程在同時運行。
3、為什么要使用多線程
使用多線程的好處如下:
1.更多處理器核心
計算機處理器核心數(shù)增多,由以前的高主頻向多核心技術發(fā)展,現(xiàn)在的計算機更擅長于并行計算,因此如何充分利用多核心處理器是現(xiàn)在的主要問題。線程是操作系統(tǒng)調度的最小單元,一個程序作為一個進程來運行,它會創(chuàng)建多個線程,而一個線程在同一時刻只能運行在一個處理器上。因此一個進程如果能使用多線程計算,將其計算邏輯分配到多個處理器核心上,那么相比單線程運行將會有更顯著的性能提升。
2.更快響應時間
在復雜業(yè)務場景中,我們可以將非強一致性關聯(lián)的業(yè)務派發(fā)給其他線程處理(或者使用消息隊列)。這樣可以減少應用響應用戶請求的時間
3.更好的編程模型
合理使用Java的提供的多線程編程模型,能使得程序員更好的解決問題,而不需要過于復雜的考慮如何將其多線程化。
4、線程的優(yōu)先級
現(xiàn)代操作系統(tǒng)基本采用的是時間片分配的方式來調度線程,也就是操作系統(tǒng)將CPU的運行分為一個個時間片,線程會分配的若干時間片,當線程時間片用完了,就會發(fā)生線程調度等待下次時間片的分配。線程在一次CPU調度中能執(zhí)行多久,取決于所分時間片的多少,而線程優(yōu)先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。
在Java線程中,線程的優(yōu)先級的可設置范圍是1-10,默認優(yōu)先級是5,理論上優(yōu)先級高的線程分配時間片數(shù)量要優(yōu)先于低的線程(部分操作系統(tǒng)這個設置是不生效的);
示例代碼:
package com.lizba.p2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 線程優(yōu)先級設置
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 12:03
*/
public class Priority {
/** 線程執(zhí)行流程控制開關 */
private static volatile boolean notStart = true;
/** 線程執(zhí)行流程控制開關 */
private static volatile boolean notEnd = true;
public static void main(String[] args) throws InterruptedException {
List<Job> jobs = new ArrayList<>();
// 設置5個優(yōu)先級為1的線程,設置5個優(yōu)先級為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)先級
*/
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時間片,等待下次調度
Thread.yield();
}
while (notEnd) {
// 讓出CPU時間片,等待下次調度
Thread.yield();
jobCount++;
}
}
}
}
執(zhí)行結果:

從輸出結果上來看,優(yōu)先級為1的線程和優(yōu)先級為10的線程執(zhí)行的次數(shù)非常相近,因此這表明程序正確性是不能依賴線程的優(yōu)先級高低的。
5、線程的狀態(tài)
線程的生命周期如下:
| 狀態(tài)名稱 | 說明 |
|---|---|
| NEW | 初始狀態(tài),線程被構建,并未調用start()方法 |
| RUNNABLE | 運行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)統(tǒng)稱為“運行中” |
| BLOCKED | 阻塞狀態(tài),線程阻塞于鎖 |
| WAITING | 等待狀態(tài),線程進入等待狀態(tài),進入該狀態(tài)表示當前線程需要等待其他線程作出一些特定動作(通知或中斷) |
| TIME_WAITING | 超時等待,先比WAITING可以在指定的時間內自行返回 |
| TERMINATED | 終止狀態(tài),表示當前線程已經執(zhí)行完畢 |
通過代碼來查看Java線程的狀態(tài)
代碼示例:
package com.lizba.p2;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 睡眠指定時間工工具類
* </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一個獲取鎖成功,一個獲取失敗
new Thread(new Blocked(), "Blocked1Thread").start();
new Thread(new Blocked(), "Blocked2Thread").start();
}
// 線程不斷的進行睡眠
static class TimeWaiting implements Runnable {
@Override
public void run() {
while (true) {
SleepUtil.sleepSecond(100);
}
}
}
// 線程等待在Waiting.class實例上
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實例上加鎖,不會釋放該鎖
static class Blocked implements Runnable {
@Override
public void run() {
synchronized (Blocked.class) {
while (true) {
SleepUtil.sleepSecond(100);
}
}
}
}
}
使用JPS查看Java進程:

查看示例代碼ThreadStateDemo進程ID是2576,鍵入jstack 2576查看輸出:

整理輸出結果: 
| 線程名稱 | 線程狀態(tài) |
|---|---|
| Blocked2Thread | BLOCKED (on object monitor),阻塞在獲取Blocked.class的鎖上 |
| Blocked1Thread | TIMED_WAITING (sleeping) |
| WaitingThread | WAITING (on object monitor) |
| TimeWaitingThread | TIMED_WAITING (sleeping) |
總結:
線程在自身生命周期中不是規(guī)定處于某一個狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進行切換。
Java線程的狀態(tài)變化圖如下:

總結:
- 線程創(chuàng)建后,調用start()方法開始運行
- 線程執(zhí)行wait()方法后,線程進入等待狀態(tài),進入等待的線程需要依靠其他線程才能夠返回到運行狀態(tài)
- 超時等待相當于在等待狀態(tài)的基礎上增加了超時限制,達到設置的超時時間后返回到運行狀態(tài)
- 線程執(zhí)行同步方法或代碼塊時,未獲取到鎖的線程,將會進入到阻塞狀態(tài)。
- 線程執(zhí)行完Runnable的run()方法之后進入到終止狀態(tài)
- 阻塞在Java的concurrent包中Lock接口的線程是等待狀態(tài),因為Lock接口阻塞的實現(xiàn)使用的是Daemon線程
6、Daemon線程
簡介:
Daemon線程是一種支持型線程,它的主要作用是程序中后臺調度和支持性工作。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。Daemon線程需要在啟動之前設置,不能在啟動之后設置。
設置方式:
Thread.setDaemon(true)
需要特別注意的點:
Daemon線程被用作支持性工作的完成,但是在Java虛擬機退出時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();
}
}
輸出結果:

總結:
不難發(fā)現(xiàn),DaemonRunner的run方法的finally代碼塊并沒有執(zhí)行,這是因為,當Java虛擬機中已經沒有非Daemon線程時,虛擬機會立即退出,虛擬機中的所以daemon線程需要立即終止,所以線程DaemonRunner會被立即終止,finally并未執(zhí)行。
二、線程啟動和終止
1、構造線程
運行線程之前需要構造一個線程對象,線程對象在構造的時候需要設置一些線程的屬性,這些屬性包括線程組、線程的優(yōu)先級、是否是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");
}
// 設置線程名稱
this.name = name;
// 當前線程設置為該線程的父線程
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();
// 設置線程組
this.group = g;
// 將daemon屬性設置為父線程的對應的屬性
this.daemon = parent.isDaemon();
// 將prority屬性設置為父線程的對應的屬性
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);
// 復制父線程的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;
// 設置一個線程id
tid = nextThreadID();
}
總結:
在上述代碼中,一個新構建的線程對象時由其parent線程來分配空間的,而child繼承了parent是否為Daemon、優(yōu)先級和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時會分配一個唯一的ID來標志線程。此時一個完整的能夠運行的線程對象就初始化好了,在堆內存中等待運行。
2、什么是線程中斷
中斷可以理解為線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以通過調用靜態(tài)方法Thread.interrupted()對當前線程的中斷標志位進行復位。
如下情況不能準確判斷線程是否被中斷過:
1.線程已經終止運行,即使被中斷過,isInterrupted()方法也會返回false
2.方法拋出InterruptedException異常,即使被中斷過,調用isInterrupted()方法將會返回false,這是因為拋出InterruptedException之前會清除中斷標志。
示例代碼:
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);
// 啟動兩個線程
sleepThread.start();
busyThread.start();
// 休眠5秒,讓sleepThread和busyThread運行充分
SleepUtil.sleepSecond(5);
// 中斷兩個線程
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) {}
}
}
}
查看運行結果:

總結:
拋出InterruptedException的是sleepThread線程,雖然兩者都被中斷過,但是sleepThread線程的中斷標志返回的是false,這是因為TimeUnit.SECONDS.sleep(seconds)會拋出InterruptedException異常,拋出異常之前,sleepThread線程的中斷標志被清除了。但是,busyThread一直在運行沒有拋出異常,中斷位沒有被清除。
3、suspend()、resume()和stop()
舉例:線程這三個方法,相當于QQ音樂播放音樂時的暫停、恢復和停止操作。(注意這些方法已經過期了,不建議使用。)
示例代碼:
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);
// 恢復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);
}
}
}
}
輸出結果:

總結:
上述代碼執(zhí)行輸出的結果,與API說明和我們的預期完成一致,但是看似正確的代碼卻隱藏這很多問題。
存在問題:
- suspend()方法調用后不會釋放已占有的資源(比如鎖),可能會導致死鎖
- stop()方法在終結一個線程時不能保證資源的正常釋放,可能會導致程序處于不確定的工作狀態(tài)
4、正確的終止線程
- 調用線程的interrupt()方法
- 使用一個Boolean類型的變量來控制是否停止任務并終止線程
示例代碼:
package com.lizba.p2;
/**
* <p>
* 標志位終止線程示例代碼
* </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);
}
/**
* 關閉
*/
public void cancel() {
on = false;
}
}
}
輸出結果:

總結:
main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這兩種方法終止線程的好處是能讓線程在終止時有機會去清理資源。做法更加安全和優(yōu)雅。
相關文章
java實現(xiàn)ftp上傳 如何創(chuàng)建文件夾
這篇文章主要為大家詳細介紹了java實現(xiàn)ftp上傳的相關資料,教大家如何創(chuàng)建文件夾?具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
eclipse輸出Hello World的實現(xiàn)方法
這篇文章主要介紹了eclipse輸出Hello World的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11
Java 圖文并茂講解主方法中的String[] args參數(shù)作用
很多老鐵不清楚JAVA主方法中main()里面的的參數(shù)是什么意思,以及有什么作用,接下來給大家用最通俗易懂的話來講解,還不清楚的朋友來看看吧2022-04-04
SpringBoot動態(tài)修改yml配置文件的方法詳解
這篇文章主要為大家詳細介紹了SpringBoot動態(tài)修改yml配置文件的方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
如何實現(xiàn)java8 list按照元素的某個字段去重
這篇文章主要介紹了如何實現(xiàn)java8 list按照元素的某個字段去重,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,,需要的朋友可以參考下2019-06-06

