1. 程式人生 > 其它 >【併發程式設計:執行緒池】深入簡出的帶你精通java執行緒

【併發程式設計:執行緒池】深入簡出的帶你精通java執行緒

一篇文章帶你精通java執行緒!瞭解執行緒的通訊方式、作業系統層面和java層面執行緒狀態的區別、建立停止執行緒的方式!

執行緒與程序

  • 程序:用來載入指令、管理記憶體、管理IO。作業系統會以程序為單位,分配系統資源(CPU時間片、記憶體等資源),程序是資源分配的最小單位。
  • 執行緒:有時被稱為輕量級程序(Lightweight Process,LWP),是作業系統排程(CPU排程)執行的最小單位。

程序間通訊的方式

  • 管道(pipe)及有名管道(named pipe):管道可用於具有親緣關係的父子程序間的通訊,有名管道除了具有管道所具有的功能外,它還允許無親緣關係程序間的通訊。
  • 訊號(signal):訊號是在軟體層次上對中斷機制的一種模擬,它是比較複雜的通訊方式,用於通知程序有某事件發生,一個程序收到一個訊號與處理器收到一箇中斷請求效果上可以說是一致的。
  • 訊息佇列(message queue):訊息佇列是訊息的連結表,它克服了上兩種通訊方式中訊號量有限的缺點,具有寫許可權的程序可以按照一定的規則向訊息佇列中新增新資訊;對訊息佇列有讀許可權的程序則可以從訊息佇列中讀取資訊。
  • 共享記憶體(shared memory):可以說這是最有用的程序間通訊方式。它使的多個程序可以訪問同一塊記憶體空間,不同程序可以及時看到對方程序中對共享記憶體中資料的更新。這種方式需要依靠某種同步操作,如互斥鎖和訊號量等。
  • 訊號量(semaphore):主要作為程序之間及同一種程序的不同執行緒之間的同步和互斥手段。
  • 套接字(socket):這是一種更為一般的程序間通訊機制,它可用於網路中不同機器之間的程序間通訊,應用非常廣泛。

執行緒的同步互斥

  • 執行緒同步:執行緒之間所具有的一種制約關係,一個執行緒的執行依賴另一個執行緒的訊息,當它沒有得到另一個執行緒的訊息時應等待,直到訊息到達時才被喚醒。
  • 執行緒互斥:對於共享的程序系統資源,在各單個執行緒訪問時的排它性。

執行緒同步互斥的控制方法

  • 臨界區:通過對多執行緒的序列化來訪問公共資源或一段程式碼,速度快,適合控制資料訪問。(在一段時間內只允許一個執行緒訪問的資源就稱為臨界資源)。
  • 互斥量:為協調共同對一個共享資源的單獨訪問而設計的。
  • 訊號量:為控制一個具有有限數量使用者資源而設計。
  • 事件:用來通知執行緒有一些事件已發生,從而啟動後繼任務的開始。

執行緒上下文切換(Context switch):一般要5-10毫秒

  • 下文切換是指CPU(中央處理單元)從一個程序或執行緒到另一個程序或執行緒的切換。
  • 是多工作業系統的一個基本特性。
  • 切換的耗時一般為5-10毫秒。
  • 上下文切換隻能在核心模式下發生!

作業系統層面執行緒狀態:初始狀態、可執行狀態、執行狀態、休眠狀態和終止狀態。

  • 初始狀態:指的是執行緒已經被建立,但是還不允許分配 CPU 執行。
  • 可執行(就緒)狀態:指的是執行緒可以分配 CPU 執行。在這種狀態下,真正的作業系統執行緒已經被成功建立了,所以可以分配 CPU 執行。
  • 執行狀態:獲取到CPU的時間片。
  • 休眠狀態:執行狀態的執行緒如果呼叫一個阻塞的 API(例如以阻塞方式讀檔案)或者等待某個事件(例如條件變數),那麼執行緒的狀態就會轉換到休眠狀態,同時釋放 CPU 使用權,休眠狀態的執行緒永遠沒有機會獲得 CPU 使用權。
  • 終止狀態:執行緒執行完或者出現異常就會進入終止狀態,終止狀態的執行緒不會切換到其他任何狀態,進入終止狀態也就意味著執行緒的生命週期結束了。

java的執行緒狀態

  • 取自Thread類的內部列舉
    public enum State {
        // 初始化狀態
        NEW,

        // 可執行狀態+執行狀態
        RUNNABLE,

        // 阻塞狀態
        BLOCKED,

        // 無時限等待
        WAITING,

        // 有時限等待
        TIMED_WAITING,

        // 終止狀態
        TERMINATED;
    }

面試中問你執行緒的狀態,你應該如何回答?

  • 在作業系統層面有5種,java中有6種。
  • Java執行緒中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即作業系統的休眠狀態。這三種狀態永遠沒有CPU的使用權!
  • Java執行緒中的 RUNNABLE 狀態,在作業系統中分為:可執行(就緒)狀態、執行狀態。

Java執行緒的實現方式(4種)

  • 使用 Thread類或繼承Thread類
// 建立執行緒物件
Thread t = new Thread() {
    public void run() {
    // 要執行的任務
    }
};
// 啟動執行緒
  • 實現 Runnable 介面配合Thread:執行緒(Thread)與任務(Runnable)分開
Runnable runnable = new Runnable() {
    public void run(){
    // 要執行的任務
    }
};
// 建立執行緒物件
Thread t = new Thread( runnable );
// 啟動執行緒
  • 使用有返回值的 Callable
classCallableTaskimplementsCallable<Integer>{
@Override
publicIntegercall()throwsException{
returnnewRandom().nextInt();
}
}
//建立執行緒池
ExecutorServiceservice=Executors.newFixedThreadPool(10);
//提交任務,並用Future提交返回結果
  • 使用 lambda
newThread(()->System.out.println(Thread.currentThread().getName())).start();

Thread常用方法

sleep方法

  • 呼叫 sleep 會讓當前執行緒從 Running 進入TIMED_WAITING狀態,不會釋放物件鎖
  • 其它執行緒可以使用 interrupt 方法打斷正在睡眠的執行緒,這時 sleep 方法會丟擲 InterruptedException,並且會清除中斷標誌
  • 睡眠結束後的執行緒未必會立刻得到執行
  • sleep當傳入引數為0時,和yield相同

yield方法

  • yield會釋放CPU資源,讓當前執行緒從 Running 進入 Runnable狀態,讓優先順序更高(至少是相同)的執行緒獲得執行機會,不會釋放物件鎖;
  • 假設當前程序只有main執行緒,當呼叫yield之後,main執行緒會繼續執行,因為沒有比它優先順序更高的執行緒;
  • 具體的實現依賴於作業系統的任務排程器

join方法

  • 等待呼叫join方法的執行緒結束之後,程式再繼續執行,一般用於等待非同步執行緒執行完結果之後才能繼續執行的場景。

如何正確優雅的停止執行緒?

stop方法

  • stop()方法已經被jdk廢棄,原因就是stop()方法太過於暴力,強行把執行到一半的執行緒終止。

Java執行緒的中斷機制

  • 中斷機制是一種協作機制,也就是說通過中斷並不能直接終止另一個執行緒,而需要被中斷的執行緒自己處理。
  • 被中斷的執行緒擁有完全的自主權,它既可以選擇立即停止,也可以選擇一段時間後停止,也可以選擇壓根不停止。
  • interrupt(): 將執行緒的中斷標誌位設定為true,不會停止執行緒
  • isInterrupted(): 判斷當前執行緒的中斷標誌位是否為true,不會清除中斷標誌位
  • Thread.interrupted():判斷當前執行緒的中斷標誌位是否為true,並清除中斷標誌位,重置為fasle
  • sleep可以被中斷 丟擲中斷異常:sleep interrupted, 清除中斷標誌位
  • wait可以被中斷 丟擲中斷異常:InterruptedException, 清除中斷標誌位

Java執行緒間通訊方式

  • volatile:兩大特性,一是可見性,二是有序性,禁止指令重排序,其中可見性就是可以讓執行緒之間進行通訊。
  • 等待喚醒(等待通知)機制:基於wait和notify方法來實現,在一個執行緒內呼叫該執行緒鎖物件的wait方法,執行緒將進入等待佇列進行等待直到被喚醒。
  • LockSupport:JDK中用來實現執行緒阻塞和喚醒的工具,執行緒呼叫park則等待“許可”,呼叫unpark則為指定執行緒提供“許可”。
  • 管道輸入輸出流:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前兩種面向位元組,而後兩種面向字元。
  • Thread.join:可以簡單理解為執行緒合併。一個執行緒等待到另一個執行緒執行完。

為什麼說本質上Java中實現執行緒只有一種方式?


    private Runnable target;

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  • 繼承Thread類,其實就是使用的Runnable物件呼叫run方法
public interface ThreadFactory {

    Thread newThread(Runnable r);
}
  • 執行緒池的建立,本質上也是傳入一個Runnable物件
  • 使用lambda的方式,本質與繼承的方式一致
  • 所以,建立執行緒的本質只有一種!

簡單瞭解start()方法是如何開啟一個執行緒的(JVM與作業系統層面)?

  • 使用new Thread()建立一個執行緒,然後呼叫start()方法進行java層面的執行緒啟動;
  • 呼叫本地方法start0(),去呼叫jvm中的JVM_StartThread方法進行執行緒建立和啟動;
  • 呼叫new JavaThread(&thread_entry, sz)進行執行緒的建立,並根據不同的作業系統平臺呼叫對應的os::create_thread方法進行執行緒建立;
  • 新建立的執行緒狀態為Initialized,呼叫了sync->wait()的方法進行等待,等到被喚醒才繼續執行thread->run();
  • 呼叫Thread::start(native_thread);方法進行執行緒啟動,此時將執行緒狀態設定為RUNNABLE,接著呼叫os::start_thread(thread),根據不同的作業系統選擇不同的執行緒啟動方式;
  • 執行緒啟動之後狀態設定為RUNNABLE, 並喚醒第4步中等待的執行緒,接著執行thread->run()的方法;
  • JavaThread::run()方法會回撥第1步new Thread中複寫的run()方法。

結束語

  • 獲取更多有價值的文章,讓我們一起成為架構師!
  • 關注公眾號,可以讓你逐步對MySQL以及併發程式設計有更深入的理解!
  • 這個公眾號,無廣告!!!每日更新!!!