目錄
線程的啓動和終止
不熟悉線程基本概念的同學,可以先看看我的上一篇文章拜託,學習併發編程之前請學習下線程!
1、線程的構造
在運行線程之前首先要構造一個線程對象,java.Lang.Thread中爲我們提供了一個用於創建線程時的初始化方法。主要對線程中的屬性進行初始化
主要的屬性
ThreadGroup g:線程組
Runnable target:可以調用run方法的對象
String name:構造的線程名字
long stackSize:新線程所需的堆棧大小,參數爲0時表示被忽略
主要源碼解析
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();
//線程組
this.group = g;
//使用父線程的Damon、Priority屬性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
//加載父線程的contextClassLoader
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);
//從父線程中拿到inheritThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
//分配一個線程ID
tid = nextThreadID();
}
這表明一個新的構造的線程對象是由其parent線程來進行空間分配的,而child線程繼承了parent是否爲Daemon、優先級和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程,至此一個能夠運行的線程就初始化好了。
講解了初始化方式後,我們來學習下怎麼多線程編程?有哪些方式?這些方式之間有什麼特點?
1.1繼承Thread類
創建一個類去繼承Thread類,重寫裏的run方法,在main方法中調用該類的實例對象的start方法就可以實現多線程的併發
測試代碼
**
* @Author: Simon Lang
* @Date: 2020/5/2 17:05
*/
public class UseThread {
public static void main(String[] args){
MyThread thread1=new MyThread("線程A");
MyThread thread2=new MyThread("線程B");
thread1.start();
thread2.start();
}
}
//構造MyThread線程
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run() {
for (int i=1;i<=3;i++){
System.out.println("線程"+name+" :"+i);
}
}
}
測試結果
第一次測試
第二次測試
我們測試了兩次結果,因爲線程是併發執行的,所以結果可能是不同的,運行的結果與代碼的執行順序或者調用順序無關。
1.2實現Runnable接口
/**
* @Author: Simon Lang
* @Date: 2020/5/2 17:12
*/
public class UseRunnable {
public static void main(String[] args){
MyRunnable runnable1=new MyRunnable("線程C");
MyRunnable runnable2=new MyRunnable("線程D");
Thread thread1=new Thread(runnable1);
Thread thread2=new Thread(runnable2);
thread1.start();
thread2.start();
}
}
class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name){
this.name=name;
}
public void run() {
for (int i=1;i<=3;i++){
System.out.println(name+" :"+i);
}
}
}
測試的結果也不是固定的,這裏就不貼圖了,這裏也用到了Thread類,它的作用是把run方法包裝成線程執行體,被包裝後可以使用start方法執行線程。
Note:繼承Thread類只能是單繼承,如果實現Runnable接口,可以使得開發更加的靈活。
2、啓動線程
線程的啓動時調用start()方法,此時的線程會進入就緒狀態,這表明它可以由JVM調度調度並執行,但是這並不意味着它會立即執行,當CPU給這個線程分配時間片後,就會開始run方法。
我們裏來查看看satrt方法的源碼看看都具體做了些什麼?
start方法源碼解析
start()方法執行流程:①判斷當前線程是不是首次創建②如果是,調用strat0方法(JVM)進行資源調度,回調run方法執行具體的操作
public synchronized void start() {
//不能重複執行start,否則將會拋出異常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//將該線程添加到線程組
group.add(this);
boolean started = false;
//start0()是一個native方法,這表明線程的實現與java無關
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
那麼,我們可能會有疑問,爲什麼不能直接調用run方法呢?
這是因爲start方法是用於啓動線程的,可以實現併發,而run方法只是一個普通的方法,是不能實現併發的,只是在併發執行時調用
NOTE:start()方法中的native方法是本地方法,使用C/C++寫的,這個方法的使用與java平臺無關,java所做的只是將Thread對象映射到操作系統所提供的線程上面,對外提供統一的接口。
3、線程的中斷
中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其它線程進行了中斷操作,但是通過中斷並不能直接終止另一個線程,而是需要中斷的線程自己處理。
java.lang.Thread類提供了幾個方法操作中斷狀態:
-
interrupted()
測試當前線程是否被中斷,該方法可以消除線程的中斷狀態,如果連續調用該方法,第二次的調用會返回false。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
-
isInterrupted()
測試線程是否已經中斷,中斷的線程返回true,中斷的狀態不受該方法的影響
public boolean isInterrupted() {
return isInterrupted(false);
}
-
interrupt()
中斷線程,將中斷狀態設置爲true,
public void interrupt() {
//省略,具體的操作是本地方法interrupt0
}
Note:這三個方法中,只有interrupt()方法是修改中斷標誌的,另外的兩個檢測方法都是用於檢測中斷狀態的
線程共有6中狀態,中斷主要用於運行態/阻塞態/等待態/超時等待,這四種狀態的中斷機制下又可分爲兩類。一類是設置中斷標誌,讓被中斷的線程判斷後執行相應的代碼。另一類是遇到中斷時拋出InterruptedException異常,並清空中斷標誌位。
3.1運行態的中斷/阻塞態中斷
對處於運行態和阻塞態執行中斷後,它們的執行狀態不變,但是中斷標誌位已被修改,並不會實際的中斷線程的運行,我們可以利用java程序中的中斷標誌位來進行自我中斷,因爲這兩種狀態的操作都是類似的,所以我們只講解運行態。
測試代碼
public class TestInterrupte {
public static void main(String[] args) throws InterruptedException {
//創建一個子線程
MyThreadA thread=new MyThreadA();
//開啓這個子線程
thread.start();
//打印中斷前線程的狀態和標誌位
System.out.println(thread.getState());
System.out.println(thread.isInterrupted());
thread.interrupt();
//中斷後線程的執行狀態和標誌位
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
//MyThreadA子線程
class MyThreadA extends Thread{
@Override
public void run() {
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("該線程執行中斷");
break;
}
}
}
}
測試結果
Note:從測試結果可以看出,線程的中斷不會立馬影響線程的狀態,線程中斷前默認標誌位爲false,中斷後標誌位被修改true,標誌後被修改後,子線程並沒有馬上執行中斷,而是在主線程繼續執行一段時間後才執行中斷(從先打印RUNNABLE,後打印“該線程執行中斷”可以看出)。
3.2等待態的中斷/超時等待態的中斷
這兩種狀態很類似,它們均是在線程運行的過程中缺少某些條件而被掛載在某個對象的等待對列中,當這些線程遇到中斷操作的時候,就會拋出一個InterruptedException異常,並清空中斷標誌位,這裏以等待態爲例編寫測試代碼
測試代碼
public class TestInterrupte {
public static void main(String[] args) throws InterruptedException {
//創建一個MyThreadB子線程
MyThreadB thread=new MyThreadB();
//開啓子線程
thread.start();
//睡眠500ms,使得該線程阻塞
MyThread.sleep(500);
//打印中斷前線程的狀態和標誌位
System.out.println(thread.getState());
System.out.println(thread.isInterrupted());
thread.interrupt();
ThreadB.sleep(500);
//打印中斷前線程的狀態和標誌位
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
class MyThreadB extends Thread{
@Override
public void run() {
synchronized (this){
try {
wait();
}catch (InterruptedException e){
System.out.println("發生中斷了,我要拋出異常");
}
}
}
}
測試結果
NOTE:從結果上可以看出,線程被啓動後就被掛載到了等待隊列中了,我們執行中斷後,輸出了我們要打印的異常語句,因爲標誌位被清空,所以打印出來的標誌位時false,執行完中斷後立馬進入到阻塞態。
中斷總結:
線程中斷不會使得線程立馬退出,而是會給線程發送一個通知,告訴目標線程你需要退出了,具體的退出操作是由目標線程來執行,這也就是爲什們上面測試代碼中,從等待態--->阻塞態的原因
4、線程的終止
在早期的jdk版本時,經常使用stop方法來強制終止線程,但是這種操作是不安全的,會導致數據的丟失,所以,可以使用interrupt來中斷線程,除此之外,還可以利用中斷標誌使線程正常退出,也就是當run方法執行完後終止線程。
所以總結來說,線程的終止有三種方式
stop方法強制退出
interrupt方法中斷線程
使用退出標誌
因爲第一種方法不安全,本文將重點講解是由退出標誌位進和使用中斷來終止線程
測試代碼
public class ShutDown {
public static void main(String[] args) throws InterruptedException {
Runner one=new Runner();
Thread countThread=new Thread(one,"CountThread");
countThread.start();
//睡眠1後,main線程對CountThread進行中斷,使CountThread能夠感知中斷而結束
TimeUnit.SECONDS.sleep(1);//子線程在1s不斷累加
countThread.interrupt();//採用中斷結束
Runner two=new Runner();
countThread=new Thread(two,"CountThread");
countThread.start();//重新開啓線程
//睡眠1s,main線程對Runner two進行取消,使得CountThread能夠感知on爲false而結束
TimeUnit.SECONDS.sleep(1);
two.cancel();//採用標誌位進行結束
}
private static class Runner implements Runnable{
private long i;
//定義標誌變量
private volatile boolean on=true;
public void run() {
//on爲true且沒有執行了中斷
while (on && !Thread.currentThread().isInterrupted()){
i++;
}
//執行中斷或標誌位爲false時,打印輸出
System.out.println("Count i= " +i);
}
public void cancel(){
on=false;
}
}
}
**NOTE:**程序創建了線程CountThread,它不斷的進行變量的累加,而主線程嘗試對其進行中斷操作和停止操作。分別使用了cancel方法和中斷操作來終止線程,這兩種方法都是有效的。
參考文獻
[1]方騰飛.java併發編程的藝術
[2]https://blog.csdn.net/weixin_43490440/article/details/101519957
[3]https://www.cnblogs.com/yangming1996/p/7612653.html
筆者組建了學習交流羣,關注公衆號,歡迎大家加入,一起進步。
在公衆號回覆success領取獨家整理的學習資源
看了這篇文章,你是否「博學」了
「掃碼關注我」,每天博學一點點。