[EP1][線程基礎]

[EP1][線程基礎]

進程和線程的基本概念

進程

進程是程序運行的單元 可以認爲一個程序啓動後就是一個進程——進行中的程序

線程

線程是操作系統調度資源的基本單位 是進程的一個子任務
一個進程至少包含一個線程

線程是比進程更加輕量級的調度單位,線程的引入可以把進程的資源分配和執行調度分開,各個線程既可以共享進程資源,又可以獨立調度。

知道了以上的基本概念

就明白了併發編程爲什麼是多線程編程
多線程編程實際上就是程序員調度多個線程
讓其“同時”執行任務的過程

(但是現在又出現了一種更靈活的 “輕量級進程”——協程 可以進行更簡化的併發編程)

Java線程的實質

Java的線程,運行的實質是Java虛擬機將用戶創建的用戶級線程,綁定在操作系統內核的輕量級線程或者內核級線程上,其對CPU時間的搶佔,調度都是通過操作系統內核調度來完成的

用戶級線程

User Thread,完全建立在用戶空間的線程稱爲用戶線程,用戶線程的建立、調度和銷燬都在用戶態裏完成。

優點:

  • 用戶線程的建立、調度和銷燬都在用戶態裏完成,代價低廉,可以支持大規模用戶線程併發。
    缺點:
  • 缺少內核的支持,線程的各種操作都需要自己實現,實現起來很複雜。

操作系統的線程

內核線程:Kernel Level Thread,它是直接由系統內核支持的線程,該線程有系統內核完成切換,通過調度器把每個線程映射到每個處理器上。

輕量級進程:Light Weight Process,也稱爲協程,是應用程序進行併發運行的高級程序接口,底層同樣由內核線程實現。

優點:

  • 實現簡單,線程的創建、調度與銷燬都有內核來完成。
    缺點:
  • 基於內核實現,需要進行系統調用,也就是內核態和用戶態的切換,代價相對較高,且需要消耗系統資源。

Java虛擬機的提供的線程,在主流平臺都是基於用戶線程綁定內核線程實現的,所以其進行線程切換時,不僅要切換線程上下文,還要從用戶態轉入內核態,對性能的損失還是比較大的。

創建線程

Java的線程創建有三種方式

  • 新建類繼承Thread

  • 新建類實現Runable接口

  • 新建類實現Callable接口

最基礎也是最容易理解的就是新建類繼承Thread類
直接創建一個線程並運行 最符合常人思維

查看Thread源碼

public class Thread implements Runnable

可以發現 Thread是實現了Runnable接口
所以Runnable和Thread本質上是一樣的
只是實現Runnable接口可以避免Java不能多繼承的問題

至於Callable接口 是Java 5後在Java Concurren 併發工具包中新增的內容
主要配合FutureTask進行異步編程
其和Runnable的區別是Callable有返回值

構造一個線程

  • 使用Thread

新建類繼承Thread類 構造線程 很簡單

public class TheFirstThread extends Thread{      
  @Override
  public void run(){    
        System.out.println("線程運行----->");
        System.out.println("線程終止----->");
    } 
    }

這樣一個線程就定好了
最終要的是重寫Thread 類的run()方法
run() 方法體中的內容 就是線程運行時執行的內容

運行時 執行start()方法 啓動線程

Thread theFirstThread=new TheFirstThread();
theFirstThread.start();

這樣就能運行了

結果

線程運行----->
線程終止----->
  • 使用Runnable

新建類實現Runnable接口即可

public class TheFirstRunnable implements Runnable{
    @Override
    public void run() {
          System.out.println("Runnable 線程運行----->");
          System.out.println("Runnable 線程終止----->");
      } 
    }

運行時和 Thread稍有不同

TheFirstRunnable tfr=new TheFirstRunnable();
Thread runT=new Thread(tfr);
runT.start();

以實現Runnable接口形式創建的進程
必須被Thread類加載後才能啓動運行

可以把Runnable看成是一個線程的任務
Thread是其宿主,負載Runnable來運行

運行結果

Runnable 線程運行----->
Runnable 線程終止----->
  • 使用Callable

Callable 是Java 5才引入的
其和Runnable的區別是 Runnable接口是無返回值的
而Callable接口有返回值
引入Callable接口主要是爲了異步任務
這個後面再詳談

顯然 Callable既然是有返回值的
則理所當然應該爲一個泛型接口
這裏使用了String型爲泛型限定符

public class TheFirstCallable implements Callable<String> {
    @Override
    public String call() throws Exception {    
          System.out.println("Callable 線程開始");
          System.out.println("Callable 線程結束");
          return "Callable 線程已結束";
      } 
    }

創建完畢

運行


TheFirstCallable theFirstCallable=new TheFirstCallable();
FutureTask<String> futureTask=new FutureTask(theFirstCallable);
new Thread(futureTask).start();

String s=futureTask.get();
System.out.println("異步運行結果-》"+s);

與Thread ,Runnable都不同
Callable 必須先被FutureTask 未來任務加載
然後再有Thread搭載運行
Callable是一個回調的機制 運行完畢後回調 未來任務 發送結果
所以最後調用FutureTask來獲取結果

結果

Callable 線程開始
Callable 線程結束
異步運行結果-》Callable 線程已結束

線程的基本操作

線程名與線程標識符

線程可以設置線程名
以便爲用戶提供一個友好的區分手段

Thread形式

Thread theFirstThread=new TheFirstThread();
theFirstThread.setName("第一個thread");
System.out.println(theFirstThread.getName());

Runnable形式
可以在構造時傳入 或者後面調用set方法設置

TheFirstRunnable tfr=new TheFirstRunnable();
Thread runT=new Thread(tfr,"第一個Runnable");
runT.setName("第一個Runnable");
runT.start();

Callable同理

而線程的唯一標識符是其ID

Thread theFirstThread=new TheFirstThread();
theFirstThread.start();
System.out.println(theFirstThread.getId());

與線程名不同 標識符是JVM自動分配的
無法更改

currentThread() 方法

Thread.currentThread() 方法返回正在調用代碼段的線程

從前面的介紹可知 通常情況下 我們編寫的是都是需要併發(重複)執行的代碼段
然後構造新的Thread類搭載代碼段任務去執行
那麼如果需要在代碼中判斷此時加載代碼段的是哪個線程

那就需要調用currentThread()方法

currentThread()方法是Thread類的一個靜態方法

public class TheCurrentRunnable implements Runnable{
      @Override
      public void run() {    
          String name=Thread.currentThread().getName();
          Long id=Thread.currentThread().getId();
          System.out.printf("%s %d",name,id);
          System.out.println();
        } 
    }

運行

TheFirstRunnable tfr=new TheFirstRunnable();
for (int i=0;i<10;i++) {
  Thread thread = new Thread(tfr, "第" + i + "個線程");
    thread.start();
    thread.join();
}

結果

第0個線程 14
第1個線程 15
第2個線程 16
第3個線程 17
第4個線程 18
第5個線程 19
第6個線程 20
第7個線程 21
第8個線程 22
第9個線程 23

sleep()方法

Thread.sleep() 方法讓線程休眠 在指定時間內停止運行

sleep()方法會讓線程休眠 讓出CPU時間片以供其他線程運行
但是並不會釋放資源和鎖 休眠時間到後自動恢復運行

調用從方法很簡單

System.out.println("線程運行----->");
try {
  System.out.println("線程休眠----->");
    Thread.sleep(5);
} catch (InterruptedException e) {
  e.printStackTrace();
} System.out.println("線程終止----->");

但涉及到多線程資源競爭問題 後面再詳細研究

isAlive()方法

isAlive()方法 獲取線程是否還活動的狀態

線程自身代碼同上

測試代碼如下

Thread theFirstThread = new TheFirstThread();
theFirstThread.start();
System.out.println(theFirstThread.isAlive());

結果

true
線程運行----->
線程休眠----->

停止和銷燬線程

線程的停止 並不是一件簡單的事 並不能直接粗暴的終止線程
因爲線程停止時 必須釋放資源和鎖 否則可能會引起其他問題
如死鎖

廢棄的stop(),suspend(),resume()方法

查看Thread的源碼 發現源碼中有三個方法
停止stop方法,暫停supend方法,恢復resume方法

@Deprecated(since="1.2") 
public final void stop()


@Deprecated(since="1.2") 
public final void suspend()

@Deprecated(since="1.2") 
public final void resume()

有點像播放器的按鈕
當然也確實是類比播放器來設置的
以停止來終止線程
暫停和恢復來控制線程運行

但是 這三個方法已經標註爲廢棄@Deprecated
(雖然從Jdk 1.2就標準廢棄 但到了Java 11還沒有刪掉 手動滑稽)

比如 suspend()方法在暫停線程時 並不會釋放資源 比如鎖
所以可能會造成死鎖問題 現在由wait方法代替

stop()方法會粗暴的停止線程 並不會保證線程佔有的資源
安全的正常的釋放

因爲這三個方法是有問題的
所以Java官方並不建議我們使用這三個方法來控制線程

interrupt() 中斷方法

那麼怎麼停止線程呢
Java提供了 interrupt()中斷方法

public void interrupt()

調用代碼

LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();

看起來還是很簡單的
不夠事實上並不是那麼容易
中斷並不是終止線程 而是給目標線程打上一個終止標記

所以調用interrupt()中斷方法 並不能保證線程立即終止

所以 真正的終止 在下一節 安全的終止線程

線程通過檢查自身是否被中斷來進行響應,通過方法isInterruped()
來進行判斷是否被中斷,也可以調用靜態方法Thread.interruped()
對當前線程的中斷標識位進行復位。

如果該線程已經終止 此時調用該對象的isInterruped()方法只
會返回false

LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
TimeUnit.SECONDS.sleep(1);
timeRunThread.interrupt();

Boolean isInterrupt=timeRunThread.isInterrupted();
System.out.println(isInterrupt); //false

結果就是 false

在線程拋出中斷異常InterruptException時 虛擬機會先將異常標誌
位清除 然後拋出InterruptException 則此時調用isInterruped()方
會返回false


public class InterruptExceptionThread extends Thread{    

  @Override
  public void run(){    
      try {
            throw new InterruptedException();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }    
        
        try {
              Thread.sleep(5000);
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
 } 
 }

測試代碼


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
TimeUnit.SECONDS.sleep(6);

timeRunThread.interrupt();
Boolean isInterrupt=timeRunThread.isInterrupted();
System.out.println(isInterrupt);

結果

java.lang.InterruptedException
	at xyz.my.LongTimeRunThread.run(LongTimeRunThread.java:21)
	
false

安全的停止線程

停止線程是多線程開發重要的技術點,停止一個線程即在線程處理任務並未完成之前,放棄當前的操作,因爲停止線程並不像跳出循環那麼簡單幹脆,而是需要一些技巧性的處理。

在Java中 可以使用三種方法推出正在運行的線程

  1. 使用退出標誌 使線程執行完 run()方法正常退出
  2. 可以使用Thread.stop()方法粗暴的停止線程,但是前面說個 這個方法是不安全的。
  3. 可以使用Thread.interrupt()方法中斷線程,但是前面也說個 此方法並不是真正停止線程 而是打上一個中斷標記

使用退出標誌 正常退出線程

使用退出標誌是終止線程中最容易理解的一種
因爲其邏輯是正常的運行完分支內容並退出
和其他的類中的方法 沒有什麼區別

線程代碼

public class StopThread implements Runnable {    

    private String runFlag;
    public StopThread(String runFlag) {
      this.runFlag=runFlag;
    }    
    
    public String getRunFlag() {
      return runFlag;
    }    
    
    public void setRunFlag(String runFlag) {
      this.runFlag = runFlag;
    }    
    
    @Override
    public void run() {    
    while ("run".equals(runFlag)) {    
          System.out.println("線程還在運行");
        }
      System.out.println("線程終止");
    } 
}

運行代碼


StopThread stopThread = new StopThread("run");
Thread thread = new Thread(stopThread,"1");
thread.start();
Thread.sleep(1);
stopThread.setRunFlag("stop");

結果

線程還在運行
線程還在運行
線程還在運行
線程還在運行
線程還在運行
線程終止

可以看出 其邏輯還是比較清晰明瞭的

但實際操作起來並不是這麼容易
這段簡單的代碼是依靠死循環來強制
校驗變量是否變化以達到進入終止分
支的目的 而衆所周知,死循環是十分
耗費系統資源的

所以 在實際編程中 如何控制 還需更多考慮

使用interrupt()方法停止線程

使用中斷法退出 前面在介紹interrupt()方法
時已經說明 調用interrupt()方法並不是真的
停止了線程 而是在線程上打上停止標記
具體何時停止要看虛擬機的調度情況

知道了怎麼停止線程 還要看線程是否停止
前面已經提到 Thread提供了兩個方法
檢測線程是否終止

分別是

檢測當前線程是否已中斷

public static boolean interrupted()

檢測線程是否已中斷

public boolean isInterrupted()

這兩個方法的區別是什麼 在其Java doc
中已經寫的非常清楚了

總結下

  1. interrupted() 檢測當前線程是否是已中斷的狀態
    執行後將清除中斷狀態位(將其置爲false)
  2. isInterrupted() 檢測對象線程是否是已中斷的狀態
    但不清除中斷位狀態標誌

實例
isInterrupted()


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();
Boolean isInterrupt=timeRunThread.isInterrupted();
Boolean isInterrupts=timeRunThread.isInterrupted();
System.out.println(isInterrupt); // true
System.out.println(isInterrupts); // true

interrputed()

與isInterrupted() 這是一個靜態方法 主要在線程自身中調用


public class LongTimeRunThread extends Thread{    
  @Override
  public void run(){    
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted()); //true
        System.out.println(Thread.interrupted()); //false

    } }

使用異常法停止線程

前面提到 調用interrupt()方法並不是真的
停止了線程 而是在線程上打上停止標記
具體何時停止要看虛擬機的調度情況

那麼 怎樣才能立即停止線程呢
就是使用 在線程中拋出中斷異常
的方法 立即停止線程

先看看 不拋出異常的情況


public class LongTimeRunThread extends Thread{    
  @Override
  public void run(){    
  for (int i = 0; i < 100000; i++) {
      if (this.isInterrupted()){
      System.out.println("發生中斷 正在退出");
                    break;
                }
      System.out.println(i++);
        }
    System.out.println("已中斷 但未立即退出");
    } 
}


運行


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();

結果

發生中斷 正在退出
已中斷 但未立即退出

從運行結果 可知 線程發生中斷時並不是立即退出的

下面 使用異常停止

public class LongTimeRunThread extends Thread {    
  @Override
  public void run() {    
        try {
            for (int i = 0; i < 100000; i++) {
            if (this.isInterrupted()) {
                System.out.println("發生中斷 正在退出");
                throw new InterruptedException();
            }
              System.out.println(i++);
            }
        System.out.println("已中斷 但未立即退出");
        }
        catch (InterruptedException e){    
           e.printStackTrace();
       }
  } 
}

運行代碼同上

結果


發生中斷 正在退出
java.lang.InterruptedException
	at xyz.my.LongTimeRunThread.run(LongTimeRunThread.java:20)

拋出異常後立即就停止了

在沉睡中停止

如果線程在sleep狀態下 停止 會發生什麼?

public class SleepThread extends Thread {    
  @Override
  public void run() {
  try {
            System.out.println("run");
            Thread.sleep(100000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }     
     } 
}
SleepThread sleepThread=new SleepThread();
sleepThread.start();
sleepThread.interrupt();

結果

run
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at xyz.my.SleepThread.run(SleepThread.java:16)

和普通的中斷無太大區別 只是指出了拋出異常時的狀態 “sleep”

使用stop()強制停止線程

雖然自Java 2開始 stop()方法 就被聲明爲廢棄
但目前 Java 11時 stop()方法 仍未被刪除

使用很簡單

LongTimeRunThread longTimeRunThread = new LongTimeRunThread();
longTimeRunThread.start();
longTimeRunThread.stop();

但是stop方法停止線程是非常粗暴的
強制停止可能會造成鎖生效
引起數據異常
所以並不應該在正常的程序中使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章