Java併發程式設計-執行緒基礎

AnonyStar發表於2020-10-10

1. 執行緒的建立

首先我們來複習我們學習 java 時接觸的執行緒建立,這也是面試的時候喜歡問的,有人說兩種也有人說三種四種等等,其實我們不能去死記硬背,而應該深入理解其中的原理,當我們理解後就會發現所謂的建立執行緒實質都是一樣的,在我們面試的過程中如果我們能從本質出發回答這樣的問題,那麼相信一定是個加分項!好了我們不多說了,開始今天的 code 之路

1.1 **繼承 Thread 類建立執行緒 **

**

  • 這是我們最常見的建立執行緒的方式,通過繼承 Thread 類來重寫 run 方法,


程式碼如下:


/**
 * 執行緒類
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class ThreadDemo extends Thread {
    @Override
    public void run() {
        //執行緒執行內容
        while (true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThredDemo 執行緒正在執行,執行緒名:"+ Thread.currentThread().getName());
        }
    }
}

測試方法:

    @Test
    public void thread01(){
        Thread thread = new ThreadDemo();
        thread.setName("執行緒-1 ");
        thread.start();

        while (true){
            System.out.println("這是main主執行緒:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

結果:

繼承 Thread 的執行緒建立簡單,啟動時直接呼叫 start 方法,而不是直接呼叫 run 方法。直接呼叫 run 等於呼叫普通方法,並不是啟動執行緒

1.2 **實現 Runnable 介面建立執行緒 **

**

  • 上述方式我們是通過繼承來實現的,那麼在 java 中提供了 Runnable 介面,我們可以直接實現該介面,實現其中的 run 方法,這種方式可擴充套件性更高


程式碼如下:


/**
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class RunnableDemo implements Runnable {
 
    @Override
    public void run() {
        //執行緒執行內容
        while (true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("RunnableDemo 執行緒正在執行,執行緒名:"+ Thread.currentThread().getName());
        }
    }
}

測試程式碼:

    @Test
    public void runnableTest(){
        // 本質還是 Thread ,這裡直接 new Thread 類,傳入 Runnable 實現類
        Thread thread = new Thread(new RunnableDemo(),"runnable子執行緒 - 1");
        //啟動執行緒
        thread.start();

        while (true){
            System.out.println("這是main主執行緒:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

執行結果:

1.3 實現 Callable 介面建立執行緒


  • 這種方式是通過 實現 Callable 介面,實現其中的 call 方法來實現執行緒,但是這種執行緒建立的方式是依賴於 ** **FutureTask **包裝器**來建立 Thread , 具體來看程式碼


程式碼如下:


/**
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class CallableDemo implements Callable<String> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    @Override
    public String call() throws Exception {
        //執行緒執行內容
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("CallableDemo 執行緒正在執行,執行緒名:"+ Thread.currentThread().getName());

        return "CallableDemo 執行結束。。。。";
    }
}

測試程式碼:

    @Test
    public void callable() throws ExecutionException, InterruptedException {
        //建立執行緒池
        ExecutorService service = Executors.newFixedThreadPool(1);
        //傳入Callable實現同時啟動執行緒
        Future submit = service.submit(new CallableDemo());
        //獲取執行緒內容的返回值,便於後續邏輯
        System.out.println(submit.get());
        //關閉執行緒池
        service.shutdown();
        //主執行緒
        System.out.println("這是main主執行緒:" + Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

結果:

有的時候,我們可能需要讓一步執行的執行緒在執行完成以後,提供一個返回值給到當前的主執行緒,主執行緒需要依賴這個值進行後續的邏輯處理,那麼這個時候,就需要用到帶返回值的執行緒了



關於執行緒基礎知識的如果有什麼問題的可以在網上查詢資料學習學習!這裡不再闡述

2. 執行緒的生命週期

  • Java 執行緒既然能夠建立,那麼也勢必會被銷燬,所以執行緒是存在生命週期的,那麼我們接下來從執行緒的生命週期開始去了解執行緒。

2.1 執行緒的狀態

2.1.1 執行緒六狀態認識


執行緒一共有 6 種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

  • NEW:初始狀態,執行緒被構建,但是還沒有呼叫 start 方法

  • RUNNABLED:執行狀態,JAVA 執行緒把作業系統中的就緒和執行兩種狀態統一稱為“執行中”

  • BLOCKED:阻塞狀態,表示執行緒進入等待狀態, 也就是執行緒因為某種原因放棄了 CPU 使用權,阻塞也分為幾種情況

    • 等待阻塞:執行的執行緒執行 wait 方法,jvm 會把當前執行緒放入到等待佇列➢ 同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被其他執行緒鎖佔用了,那麼 jvm 會把當前的執行緒放入到鎖池中
    • 其他阻塞:執行的執行緒執行 Thread.sleep 或者 t.join 方法,或者發出了 I/O 請求時,JVM 會把當前執行緒設定為阻塞狀態,當 sleep 結束、join 執行緒終止、io 處理完畢則執行緒恢復
  • TIME_WAITING:超時等待狀態,超時以後自動返回

  • TERMINATED:終止狀態,表示當前執行緒執行完畢


2.1.2 程式碼實操演示

  • 程式碼:

    public static void main(String[] args) {
        ////TIME_WAITING 通過 sleep wait(time) 來進入等待超時中
        new Thread(() -> {
           while (true){
               //執行緒執行內容
               try {
                   TimeUnit.SECONDS.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        },"Time_Waiting").start();
        //WAITING, 執行緒在 ThreadStatus 類鎖上通過 wait 進行等待
        new Thread(() -> {
            while (true){
                synchronized (ThreadStatus.class){
                    try {
                        ThreadStatus.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"Thread_Waiting").start();

        //synchronized 獲得鎖,則另一個進入阻塞狀態 blocked
        new Thread(() -> {
            while (true){
                synchronized(Object.class){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"Object_blocked_1").start();
        new Thread(() -> {
            while (true){
                synchronized(Object.class){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"Object_blocked_2").start();
    }

啟動一個執行緒前,最好為這個執行緒設定執行緒名稱,因為這樣在使用 jstack 分析程式或者進行問題排查時,就會給開發人員提供一些提示

2.1.3 執行緒的狀態堆疊


➢ 執行該示例,開啟終端或者命令提示符,鍵入“ jps ”, ( JDK1.5 提供的一個顯示當前所有 java 程式 pid 的命令)


➢ 根據上一步驟獲得的 pid ,繼續輸入 jstack pid (jstack是 java 虛擬機器自帶的一種堆疊跟蹤工具。jstack 用於列印出給定的 java 程式 ID core file 或遠端除錯服務的 Java 堆疊資訊)

3. 執行緒的深入解析

3.1 執行緒的啟動原理

  • 前面我們通過一些案例演示了執行緒的啟動,也就是呼叫 start() 方法去啟動一個執行緒,當 run 方法中的程式碼執行完畢以後,執行緒的生命週期也將終止。呼叫 start 方法的語義是當前執行緒告訴 JVM ,啟動呼叫 start 方法的執行緒。
  • 我們開始學習執行緒時很大的疑惑就是 啟動一個執行緒是使用 start 方法,而不是直接呼叫 run 方法,這裡我們首先簡單看一下 start 方法的定義,在 Thread 類中
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            //執行緒呼叫的核心方法,這是一個本地方法,native 
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	
	//執行緒呼叫的 native 方法
    private native void start0();
  • 這裡我們能看到 start 方法中呼叫了 native 方法 start0來啟動執行緒,這個方法是在 Thread 類中的靜態程式碼塊中註冊的 , 這裡直接呼叫了一個 native 方法 registerNatives
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

  • 如上圖,我們本地下載 jdk 工程,找到 src->share->native->java->lang->Thread.c 檔案

  • 上面是 Thread.c 中所有程式碼,我們可以看到呼叫了 RegisterNatives 同時可以看到 method 集合中的對映,在呼叫本地方法 start0 時,實際呼叫了 JVM_StartThread ,它自身是由 c/c++ 實現的,這裡需要在 虛擬機器原始碼中去檢視,我們使用的都是 hostpot 虛擬機器,這個可以去 openJDK 官網下載,上述介紹了不再多說
  • 我們看到 JVM_StartThread 的定義是在 jvm.h 原始碼中,而 jvm.h 的實現則在虛擬機器 hotspot 中,我們開啟 hotspot 原始碼,找到 src -> share -> vm -> prims ->jvm.cpp 檔案,在 2955 行,可以直接檢索 JVM_StartThread , 方法程式碼如下:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  bool throw_illegal_thread_state = false;

  {
    MutexLocker mu(Threads_lock);

    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running
      // <1> :獲取當前程式中執行緒的數量
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      size_t sz = size > 0 ? (size_t) size : 0;

      // <2> :真正呼叫建立執行緒的方法
      native_thread = new JavaThread(&thread_entry, sz);
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

  if (throw_illegal_thread_state) {
    THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL, "Starting null thread?");

  if (native_thread->osthread() == NULL) {
    // No one should hold a reference to the 'native_thread'.
    delete native_thread;
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        "unable to create new native thread");
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              "unable to create new native thread");
  }

  // <3> 啟動執行緒
  Thread::start(native_thread);

JVM_END

JVM_ENTRY 是用來定義 JVM_StartThread 函式的,在這個函式裡面建立了一個真正和平臺有關的本地執行緒, 上述標記 <2> 處

  • 為了進一步執行緒建立,我們在進入 new JavaThread(&thread_entry, sz) 中檢視一下具體實現過程,在 thread.cpp 檔案 1566 行處定義了 new 的方法

  • 對於上述程式碼我們可以看到最終呼叫了 os::create_thread(this, thr_type, stack_sz); 來實現執行緒的建立,對於這個方法不同平臺有不同的實現,這裡不再贅述,

  • 上面都是建立過程,之後再呼叫   Thread::start(native_thread); 在 JVM_StartThread 中呼叫,該方法的實現在 Thread.cpp

start 方法中有一個函式呼叫: os::start_thread(thread); ,呼叫平臺啟動執行緒的方法,最終會呼叫 Thread.cpp 檔案中的 JavaThread::run() 方法


3.2 執行緒的終止

3.2.1 通過標記位來終止執行緒

  • 正常我們執行緒內的東西都是迴圈執行的,那麼我們實際需求中肯定也存在想在其他執行緒來停止當前執行緒的需要,這是後我們可以通過標記位來實現,所謂的標記為其實就是 volatile 修飾的變數,著由它的可見性特性決定的,如下程式碼就是依據 volatile 來實現標記位停止執行緒

    //定義標記為 使用 volatile 修飾
    private static volatile  boolean mark = false;

    @Test
    public void markTest(){
        new Thread(() -> {
            //判斷標記位來確定是否繼續進行
            while (!mark){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("執行緒執行內容中...");
            }
        }).start();

        System.out.println("這是主執行緒走起...");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //10秒後將標記為設定 true 對執行緒可見。用volatile 修飾
        mark = true;
        System.out.println("標記位修改為:"+mark);
    }

3.2.2 通過 stop 來終止執行緒

  • 我們通過檢視 Thread 類或者 JDK API 可以看到關於執行緒的停止提供了 stop() , supend() , resume() 等方法,但是我們可以看到這些方法都被標記了 @Deprecated 也就是過時的,
  • 雖然這幾個方法都可以用來停止一個正在執行的執行緒,但是這些方法都是不安全的,都已經被拋棄使用,所以在我們開發中我們要避免使用這些方法,關於這些方法為什麼被拋棄以及導致的問題 JDK 文件中較為詳細的描述 《Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?》
  • 在其中有這樣的描述:

  • 總的來說就是:

    • 呼叫 stop() 方法會立刻停止 run() 方法中剩餘的全部工作,包括在 catchfinally 等語句中的內容,並丟擲 ThreadDeath 異常(通常情況下此異常不需要顯示的捕獲),因此可能會導致一些工作的得不到完成,如檔案,資料庫等的關閉。
    • 呼叫 stop() 方法會立即釋放該執行緒所持有的所有的鎖,導致資料得不到同步,出現資料不一致的問題。

3.2.3 通過 interrupt 來終止執行緒

  • 通過上面闡述,我們知道了使用 stop 方法是不推薦的,那麼我們用什麼來更好的停止執行緒,這裡就引出了 interrupt 方法,我們通過呼叫 interrupt 來中斷執行緒
  • 當其他執行緒通過呼叫當前執行緒的 interrupt 方法,表示向當前執行緒打個招呼,告訴他可以中斷執行緒的執行了,至於什麼時候中斷,取決於當前執行緒自己
  • 執行緒通過檢查自身是否被中斷來進行相應,可以通過 isInterrupted() 來判斷是否被中斷。


我們來看下面程式碼:

    public static void main(String[] args) {
        //建立 interrupt-1 執行緒

        Thread thread = new Thread(() -> {
            while (true) {
                //判斷當前執行緒是否中斷,
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("執行緒1 接收到中斷資訊,中斷執行緒...");
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "執行緒正在執行...");

            }
        }, "interrupt-1");
        //啟動執行緒 1
        thread.start();

        //建立 interrupt-2 執行緒
        new Thread(() -> {
            int i = 0;
            while (i <20){
                System.out.println(Thread.currentThread().getName()+"執行緒正在執行...");
                if (i == 8){
                    System.out.println("設定執行緒中斷....");
                    //通知執行緒1 設定中斷通知
                    thread.interrupt();
                }
                i ++;
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"interrupt-2").start();
    }

列印結果如下:

上述程式碼中我們可以看到,我們建立了 interrupt-1 執行緒,其中用 interrupt 來判斷當前執行緒是否處於中斷狀態,如果處於中斷狀態那麼就自然結束執行緒,這裡的結束的具體操作由我們開發者來決定。再建立 interrupt-2 執行緒,程式碼相對簡單不闡述,當執行到某時刻時將執行緒 interrupt-1 設定為中斷狀態,也就是通知 interrupt-1 執行緒。


執行緒中斷標記復位 :

在上述 interrupt-1 程式碼中如果加入 sleep 方法,那麼我們會發現程式報出 InterruptedException 錯誤,同時,執行緒 interrupt-1 也不會停止,這裡就是因為中斷標記被複位了 ,下面我們來介紹一下關於中斷標記復位相關的內容

  • 線上程類中提供了** **Thread.interrupted 的靜態方法,用來對執行緒中斷標識的復位,在上面的程式碼中,我們可以做一個小改動,對 interrupt-1 執行緒建立的程式碼修改如下:
        //建立 interrupt-1 執行緒

        Thread thread = new Thread(() -> {
            while (true) {
                //判斷當前執行緒是否中斷,
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("執行緒1 接收到中斷資訊,中斷執行緒...中斷標記:" + Thread.currentThread().isInterrupted());
                    Thread.interrupted(); // //對執行緒進行復位,由 true 變成 false
                    System.out.println("經過 Thread.interrupted() 復位後,中斷標記:" + Thread.currentThread().isInterrupted());
                    //再次判斷是否中斷,如果是則退出執行緒
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                }
                System.out.println(Thread.currentThread().getName() + "執行緒正在執行...");

            }
        }, "interrupt-1");

上述程式碼中 我們可以看到,判斷當前執行緒是否處於中斷標記為 true , 如果有其他程式通知則為 true 此時進入 if 語句中,對其進行復位操作,之後再次判斷。執行程式碼後我們發現 interrupt-1 執行緒不會終止,而會一直執行

  • Thread.interrupted 進行執行緒中斷標記復位是一種主動的操作行為,其實還有一種被動的復位場景,那就是上面說的當程式出現 InterruptedException 異常時,則會將當前執行緒的中斷標記狀態復位,在丟擲異常前, JVM 會將中斷標記 isInterrupted 設定為 false

在程式中,執行緒中斷復位的存在實際就是當前執行緒對外界中斷通知訊號的一種響應,但是具體響應的內容有當前執行緒決定,執行緒不會立馬停止,具體是否停止等都是由當前執行緒自己來決定,也就是開發者。


3.3 執行緒終止 interrupt 的原理

  • 首先我們先來看一下在 Thread 中關於 interrupt 的定義:
    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess();  //校驗是否有許可權來修改當前執行緒

            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    // <1> 呼叫 native 方法
                    interrupt0();  // set interrupt status
                    b.interrupt(this);
                    return;
                }
            }
        }

        // set interrupt status
        interrupt0();
    }
  • 上面程式碼中我們可以看到,在 interrupt 方法中最終呼叫了 Native 方法 interrupt0 ,這裡相關線上程啟動時說過,不再贅述,我們直接找到 hotspotjvm.cpp 檔案中 JVM_Interrupt 方法

  • JVM_Interrupt 方法比較簡單,其中我們可以看到直接呼叫了 Thread.cppinterrupt 方法,我們進入其中檢視

  • 我們可以看到這裡直接呼叫了  os::interrupt(thread) 這裡是呼叫了平臺的方法,對於不同的平臺實現是不同的,我們這裡如下所示,選擇 Linux 下的實現 os_linux.cpp 中,


在上面程式碼中我們可以看到,在 1 處拿到 OSThread ,之後判斷如果 interruptfalse 則在 2 處呼叫 OSThreadset_interrupted 方法進行設定,我們可以進入看一下其實現,發現在 osThread.hpp 中定義了一個成員變數 volatile jint _interrupted;set_interrupted 方法其實就是將 _interrupted 設定為 true ,之後再通過 ParkEventunpark() 方法來喚醒執行緒。具體的過程在上面進行的簡單的註釋介紹,


歡迎關注公眾號“雲棲簡碼”

本文由AnonyStar 釋出,可轉載但需宣告原文出處。
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關注微信公賬號 :雲棲簡碼 獲取更多優質文章
更多文章關注筆者部落格 :雲棲簡碼

相關文章