Java併發編程-線程基礎

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 發佈,可轉載但需聲明原文出處。
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關注微信公賬號 :雲棲簡碼 獲取更多優質文章
更多文章關注筆者博客 :雲棲簡碼

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