Java併發編程(一)之線程

前言

併發編程的目的是爲了讓程序運行得更快,但是,並不是啓動更多的線程就能讓程序最大限度地併發執行。在進行併發編程時,如果希望通過多線程執行任務讓程序運行得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬件和軟件的資源限制問題。這一塊內容也是面試核心考點之一,所以博主將以線程爲起點,從0到1一起與小夥伴們走去Java併發編程之路上走一遭!

正文

進程?線程?傻傻分不清?

何謂進程

進程通常是程序、應用的同義詞。進程的本質是一個正在執行的程序,程序運行時系統會創建一個進程,並且給每個進程分配獨立的內存地址空間保證每個進程地址不會相互干擾。同時,在 CPU 對進程做時間片的切換時,保證進程切換過程中仍然要從進程切換之前運行的位置出開始執行。所以進程通常還會包括程序計數器、堆棧指針。下面來一張圖,大致瞭解一下併發進程張什麼樣子:
在這裏插入圖片描述
有了進程以後,可以讓操作系統從宏觀層面實現多應用併發。而併發的實現是通過 CPU 時間片不端切換執行的。對於單核 CPU 來說,在任意一個時刻只會有一個進程在被CPU 調度。

既生進程,何生線程

  1. 在多核 CPU 中,利用多線程可以實現真正意義上的並行執行。
  2. 在一個應用進程中,會存在多個同時執行的任務,如果其中一個任務被阻塞,將會引起不依賴該任務的任務也被阻塞。通過對不同任務創建不同的線程去處理,可以提升程序處理的實時性。
  3. 線程可以認爲是輕量級的進程,所以線程的創建、銷燬比進程更快。

何謂線程

線程(Thread)的英文原意是“細絲”,Java語音上把“正在執行程序的主體”稱爲線程(Thread)。在一個軟件內,我們也能同時幹多個事,這些不同的功能可以同時進行,是因爲在進程內使用了多個線程。線程有時又稱之爲輕量級進程。但是創建一個線程要消耗的資源通常比創建進程少的多。
注:不是隻有Java程序處理系統上執行的才叫線程,垃圾收集器回收垃圾執行的也叫線程,還有GUI相關線程等等。

線程與進程的關係

一個進程內的多個線程會共享進程的資源,同時也會有自己私有的資源。線程必須存在於進程中,每個進程至少要有一個線程作爲程序的入口。線程是可以併發執行的,所以我們在一個軟件內也可以同時幹多個事。操作系統上通常會同時運行多個進程,每個進程又會開啓多個線程。一個進程包括由操作系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到所有的非守護線程都結束運行後才能結束。

Java中線程的分類

Java線程主要分爲兩類:用戶線程(非守護線程)和守護線程。用戶線程是應用程序運行時執行在前臺的線程,守護線程是應用程序運行執行在後臺的線程,也就是程序運行的時候在後臺爲非守護線程提供一種通用服務的線程。比如垃圾回收線程就是一個很稱職的守護者,並且這種線程並不屬於程序中不可或缺的部分。因此,當所有的非守護線程結束時,程序也就終止了,同時會殺死進程中的所有守護線程。反過來說,只要任何非守護線程還在運行,程序就不會終止。

守護線程和用戶線程的沒啥本質的區別,唯一的不同之處就在於虛擬機的離開:如果用戶線程已經全部退出運行了,只剩下守護線程存在了,虛擬機也就退出了。 因爲沒有了被守護者,守護線程也就沒有工作可做了,也就沒有繼續運行程序的必要了。

單線程程序

想一想我們讀代碼的一個過程,接下來這句話,你細品。明爲追蹤處理流程,實則實在追蹤線程。接下來我以圖解形式描述一下:
在這裏插入圖片描述
解讀代碼的過程就是一個追蹤流程的過程,也是一個追蹤線程的過程。我們將代碼比作一條沒有分支大河,無論是方法調用、for循環、if判斷還是更爲複雜的處理都無所謂,只要這個程序的處理流程從頭到尾就只有一條線,那麼這個程序就是單線程程序。在單線程程序中,“正在執行程序的主體”永遠只有一個。

線程的生命週期

線程是一個動態執行的過程,它也有一個從產生到死亡的過程。下面以流程圖方式展現一個線程完整的生命週期:
在這裏插入圖片描述

  1. 新建狀態
    使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。
  2. 就緒狀態
    當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。
  3. 運行狀態
    如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。
  4. 阻塞狀態
    如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:
    等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。
    同步阻塞:線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。
    其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
  5. 死亡狀態
    一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。

下面補一個更詳細的生命週期以及狀態變化圖:
在這裏插入圖片描述

線程的優先級

每一個 Java 線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。

public final void setPriority(int newPriority)  :  更改此線程的優先級。 
static int MAX_PRIORITY :線程可以擁有的最大優先級。  (值 爲10)
static int MIN_PRIORITY :線程可以擁有的最小優先級。  (值 爲1)
static int NORM_PRIORITY :被分配給線程的默認優先級。  (值爲5)

具有較高優先級的線程對程序更重要,並且應該在低優先級的線程之前分配處理器資源。但是,線程優先級並不能保證線程執行的順序,也就是說優先級高的線程不一定就會先執行,因爲它有很大的隨機性,只有搶到CPU資源的線程纔會執行,優先級高的線程只是搶佔到CPU資源的機會要更大一點,線程的執行順序真正取決於CPU調度器(在Java中是JVM來調度),我們是無法控制。
這裏也爲小夥伴們準備了一段代碼,你們可以多運行幾次看看效果體會一下優先級:

public class PriorityDemo {
    public static void main(String[] args) {
        //創建線程max最大
        Thread max=new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("max");
                }
            }
        };
        //創建線程min最小
        Thread min = new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("min");
                }
            }
        };
        //創建線程norm默認
        Thread norm = new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("norm");
                }
            }
        };

        max.setPriority(Thread.MAX_PRIORITY);//將線程max設置爲最大值10
        min.setPriority(Thread.MIN_PRIORITY);//將線程min設置爲最小大值1

        min.start();
        norm.start();
        max.start();
    }
}

如何創建線程

1.通過實現 Runnable 接口來創建線程

創建一個線程,最簡單的方法是創建一個實現 Runnable 接口的類。爲了實現 Runnable,一個類只需要執行一個方法調用 run(),線程創建之後調用它的 start() 方法它纔會執行。
Thread 定義了幾個構造方法,下面的這個是我們經常使用的:

//threadOb 是一個實現 Runnable 接口的類的實例
//threadName是線程的名字
Thread(Runnable threadOb,String threadName);

代碼示例:

public class RunnableDemo implements Runnable{
    private Thread thread;
    private String threadName;

    RunnableDemo( String name) {
        threadName = name;
        System.out.println("Creating " +  threadName );
    }

    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 1; i <= 2; i++) {
                System.out.println("Thread: " + threadName + ",第" + i + "次執行");
                // 讓線程睡眠一會
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }

    public void start () {
        System.out.println("Starting " +  threadName );
        if (thread == null) {
            thread = new Thread (this, threadName);
            thread.start ();
        }
    }

    public static void main(String[] args) {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();

        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}

執行結果:
在這裏插入圖片描述

2.通過繼承Thread來創建線程

創建一個類,該類繼承 Thread 類,然後創建一個該類的實例。繼承類必須重寫 run() 方法,該方法是新線程的入口點。它也必須調用 start() 方法才能執行。該方法儘管被列爲一種多線程實現方式,但是本質上也是實現了 Runnable 接口的一個實例。
代碼示例:

public class ThreadDemo extends Thread {
    private Thread thread;
    private String threadName;

    ThreadDemo(String name) {
        this.threadName = name;
        System.out.println("Creating " +  threadName );
    }

    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 1; i <= 2; i++) {
                System.out.println("Thread: " + threadName + ",第" + i + "次執行");
                // 讓線程睡眠一會
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " finish.");
    }

    public void start () {
        System.out.println("Starting " +  threadName );
        if (thread == null) {
            thread = new Thread (this, threadName);
            thread.start ();
        }
    }

    public static void main(String[] args) {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();

        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}

執行結果:
在這裏插入圖片描述
給小夥伴們留一個小思考,多執行幾次,你會發現執行結果不一樣,爲什麼呢?

3.通過 Callable 和 Future 創建線程

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

  1. 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。
  2. 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
  3. 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。
  4. 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。
    代碼示例:
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args) {
        CallableThreadTest callable = new CallableThreadTest();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        for(int i = 0;i < 3;i++) {
            System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
            if(i==2) {
                new Thread(futureTask,"有返回值的線程").start();
            }
        }
        try {
            System.out.println("子線程的返回值:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
    @Override
    public Integer call() {
        int i = 0;
        for(;i<3;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}

執行結果:
在這裏插入圖片描述

Thread常用API

被Thread對象調用的方法:

public void start()
使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
public void run()
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。
public final void setName(String name)
改變線程名稱,使之與參數 name 相同。
public final void setPriority(int priority)
更改線程的優先級。
public final void setDaemon(boolean on)
將該線程標記爲守護線程或用戶線程。
public final void join(long millisec)
等待該線程終止的時間最長爲 millis 毫秒。
public void interrupt()
中斷線程。
public final boolean isAlive()
測試線程是否處於活動狀態。

Thread類的靜態方法:

public static void yield()
暫停當前正在執行的線程對象,並執行其他線程。
public static void sleep(long millisec)
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。
public static boolean holdsLock(Object x)
當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。
public static Thread currentThread()
返回對當前正在執行的線程對象的引用。
public static void dumpStack()
將當前線程的堆棧跟蹤打印至標準錯誤流。

實例演示Thread類的一些方法,做一個“猜數字”小遊戲:

C-1:

// 通過實現 Runnable 接口創建線程
public class DisplayMessage implements Runnable {
    private String message;

    public DisplayMessage(String message) {
        this.message = message;
    }

    public void run() {
        while(true) {
            System.out.println(message);
        }
    }
}

C-2:

// 通過繼承 Thread 類創建線程
public class GuessANumber extends Thread {
    private int number;

    public GuessANumber(int number) {
        this.number = number;
    }

    public void run() {
        int counter = 0;
        int guess = 0;
        do {
            guess = (int) (Math.random() * 100 + 1);
            System.out.println(this.getName() + "猜的數字是:" + guess);
            counter++;
        } while(guess != number);
        System.out.println(this.getName() + "猜了" + counter + "次猜對正確答案!");
    }
}

C-3:

public class ThreadApiDemo {
    public static void main(String [] args) {
        Runnable hello = new DisplayMessage("Hello");
        Thread thread1 = new Thread(hello);
        thread1.setDaemon(true);
        thread1.setName("hello");
        System.out.println("Starting hello thread...");
        thread1.start();

        Runnable bye = new DisplayMessage("Goodbye");
        Thread thread2 = new Thread(bye);
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread2.setDaemon(true);
        System.out.println("Starting goodbye thread...");
        thread2.start();

        Thread thread3 = new GuessANumber(88);
        System.out.println("Starting thread3...");
        thread3.start();
        try {
            thread3.join(100);
        }catch(InterruptedException e) {
            System.out.println("Thread interrupted.");
        }
        Thread thread4 = new GuessANumber(66);
        System.out.println("Starting thread4...");
        thread4.start();
        System.out.println("main() is ending...");
    }
}

這裏執行結果太長,就不演示啦!小夥伴們可以多運行幾次看看效果!

線程的優點

恰當的使用線程,可以降低開發和維護的開銷,並且能夠提高複雜應用程序的性能。線程通過把異步的工作流程轉化爲普遍存在的順序流程,使得程序模擬人類工作和交互變得更加容易。另一方面,它可以吧複雜的、難以理解的代碼轉化爲直接、簡介的代碼,這樣更容易讀寫和維護。

線程在GUI程序中是肥腸有用的,可以用來改進用戶接口的響應性,並且在服務器應用中用來提高資源的利用率和吞吐量。它也可以簡化JVM的實現------垃圾收集器通常用於一個或多個持續工作的線程之間。大部分至關重要的Java應用都依賴於線程,某種程度上是因爲他們的組織結構需要線程。

線程無處不在

即使你的程序中沒有明顯的創建線程,你所用的框架也可能幫你創建了一些線程,這些程序調用的代碼必須是線程安全的。這一點給開發人員的設計和實現賦予了更重要的一份責任,因爲開發線程安全的類要比非線程安全的類需要更多的精力和分析。通過從框架中調用應用程序的組件,框架把併發引入了應用程序,組件總是需要訪問程序的狀態,因此要求在所有代碼路徑訪問狀態的時候,必須是線程安全的。

前方高能,心臟不好的小夥伴可以在這裏下車了啦!哈哈開個玩笑,前面的內容都是你必備的基礎,別看線程簡單,面試官要是真拿線程爲難你,你可能真的扛不住“奪命連環追問”,接下來我們就一起來對線程最核心的兩個點(一個啓動,一個終止)進行深入分析。

線程的啓動原理

前面我們通過一些案例演示了線程的啓動,也就是調用start()方法去啓動一個線程,當 run() 方法中的代碼執行完畢以後,線程的生命週期也將終止。調用 start ()方法的語義是當前線程告訴 JVM,啓動調用 start() 方法的線程。
很多小夥伴最開始可能會跟我一樣有一個疑惑,啓動一個線程爲什麼調用的是start()方法而不是run()方法,直接用一個run()方法啓動帶執行它不香嗎?那麼帶着這個疑惑,下面我們做一個簡單的分析,首先看一下start()方法的源碼:

    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 {
            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 */
            }
        }
    }

    private native void start0();//***再看這裏***

我們看到調用 start 方法實際上是調用一個 native 方法start0()來啓動一個線程,首先 start0()這個方法是在Thread 的靜態塊中來註冊的,代碼如下:

    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

有些小夥伴可能會有疑問,這個registerNatives()方法是幹嘛的?registerNatives()方法是爲了讓JVM找到你的本地函數,它們必須以某種方式命名。例如,對於java.lang.Object.registerNatives,相應的C函數名爲Java_java_lang_Object_registerNatives。通過使用registerNatives(或者更確切地說,JNI函數RegisterNatives),你可以命名你想要的C函數。看一下Thread.c的主要內容:

	static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    };
    #undef THD
    #undef OBJ
    #undef STE
    JNIEXPORT void JNICALL
    Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
    {
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
    }

看methods數組可以看出數組中存放的爲JNINativeMethod類型的結構體變量。JNINativeMethod主要是進行一個jni方法的映射關係,將native方法和真正的實現方法進行綁定。那麼它是怎麼綁定的呢?我來看數組下面的那個方法(Java_java_lang_Thread_registerNatives),它是registerNatives()對應的Jni方法,會對數組methods中的方法映射關係進行註冊,註冊之後它就會把Java中很多被native修飾的方法與具體實現這個功能的C語音方法的指針進行綁定(這句話可能比較繞,多看幾遍)。比如start0()就和JVM_StartThread進行了綁定,那麼接下來我們就去看看被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);
    //判斷Java線程是否已經啓動,如果已經啓動過,則會拋異常
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      size_t sz = size > 0 ? (size_t) size : 0;
      //如果Java線程沒有啓動就創建Java線程
      native_thread = new JavaThread(&thread_entry, sz);
      if (native_thread->osthread() != NULL) {
        native_thread->prepare(jthread);
      }
    }
  }

  ......
  Thread::start(native_thread);

JVM_END

這裏JVM_ENTRY是一個宏,用來定義JVM_StartThread 函數,可以看到函數內創建了真正的平臺相關的本地線程,其線程函數是 thread_entry。Java線程的具體創建過程就在JavaThread的構造函數中,接着看一下JavaThread的構造函數:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;                                                  
  os::create_thread(this, thr_type, stack_sz);
}

我們來看最後一句:os::create_thread(this, thr_type, stack_sz) ,看到os(操作系統)和create_thread就算看不懂C,我想大概也猜的出來這裏是在創建Java線程對應的內核線程吧!然後繼續看:

bool os::create_thread(Thread* thread, ThreadType thr_type,
                       size_t req_stack_size) {
    ......
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
    ......
    return true;
}

這裏pthread_create()的作用就是創建線程,前兩個參數先不關注它,我們來看後兩個參數分別代表什麼,第三個參數thread_native_entry創建的線程開始運行的地址,第四個參數thread代表線程,也是線程函數thread_native_entry()的參數,接着看thread_native_entry函數幹了什麼:

static void *thread_native_entry(Thread *thread) {
  ......
  thread->run();
  ......
  return 0;
}

是不是看到run()方法了,先別激動,還沒結束,這是JDK底層的run()方法,這跟我們Java中的run()有什麼關係呢?爲什麼Java中執行線程也叫作run()方法?run()方法爲什麼會執行?帶着這些疑問,繼續看:

// thread.cpp
void JavaThread::run() {
  ......
  thread_main_inner();
}

run()方法中調用了thread.cpp中的thread_main_inner()方法:

void JavaThread::thread_main_inner() {
  if (!this->has_pending_exception() &&
      !java_lang_Thread::is_stillborn(this->threadObj())) {
    {
      ResourceMark rm(this);
      this->set_native_thread_name(this->get_thread_name());
    }
    HandleMark hm(this);
    this->entry_point()(this, this);//重點看這裏
  }
  DTRACE_THREAD_PROBE(stop, this);
  this->exit(false);
  delete this;
}

這裏結合JavaThread的構造方法一起看,entry_point()方法的返回值就是創建JavaThread時傳入的一個函數thread_entry,所以這裏就相當於thread_entry(this,this),這個函數在jvm.cpp中:

// jvm.cpp
static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,//Java線程對象
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),//Java線程類
                          vmSymbols::run_method_name(),//開始調用Java線程對象的run()方法
                          vmSymbols::void_method_signature(),
                          THREAD);
}

呦呵!看到誰了?這不是JavaCalls嘛!有小夥伴可能會問JavaCalls是幹嘛的,其實它就是用來調用Java方法的。如果對這一塊不熟悉,可以點這裏JVM方法執行的來龍去脈,可以看到調用了 vmSymbolHandles::run_method_name 方法,而run_method_name是在 vmSymbols.hpp 用宏定義的,它又是怎麼對應找到Java中的run()方法的呢?各位看官接下來就解開謎底:

class vmSymbolHandles: AllStatic {
   ...
    template(run_method_name,"run")  
   ...
}

看的很清楚,這個用“雙引號”引起來的“run”,就是決定了調用Java代碼中的run()方法的關鍵!

Java線程創建調用圖
在這裏插入圖片描述

這樣分析下來,我相信小夥伴們已經很清楚start()和run()的關係了吧!線程啓動使用start(),run()只是一個回調方法而已。如果你直接調用run()方法,JVM是不會去創建線程的,run()方法只能用於已有線程中。Java裏面創建線程之後必須要調用start方法才能真正的創建一個線程,該方法會調用虛擬機啓動一個本地線程,本地線程的創建會調用當前系統創建線程的方法進行創建,並且線程被執行的時候會回調 run方法進行業務邏輯的處理。

優雅的線程終止

如何優雅的終止一個線程,也是面試當中經常會被問到的一個問題,接下來我們就研究一下如何優雅的終止一個線程。線程的終止,並不是簡單的調用 stop 命令去。雖然 api 仍然可以調用,但是和其他的線程控制方法如 suspend、resume 一樣都是過期了的不建議使用,就拿 stop 來說,stop 方法在結束一個線程時並不會保證線程的資源正常釋放,因此會導致程序可能出現一些不確定的狀態(相當於我們在linux上通過kill -9強制結束一個進程)。要優雅的去中斷一個線程,在線程中提供了一個 interrupt方法。

interrupt 方法

當其他線程通過調用當前線程的 interrupt 方法,表示向當前線程打個招呼,告訴他可以中斷線程的執行了,至於什麼時候中斷,取決於當前線程自己。線程通過檢查自身是否被中斷來進行相應,可以通過
isInterrupted()來檢查自身是否被中斷。
下面我們通過一個實例感受一下線程的終止:

public class InterruptDemo {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                //默認情況下isInterrupted 返回 false、通過 thread.interrupt 變成了 true
                i++;
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt(); //加和不加的效果小夥伴們可以感受一下
    }
}

執行結果就是一個簡單的打印,打印了線程啓動到終止過程中循環執行的次數。通過簡單的代碼分析我們不難看出,通過 thread.interrupt()方法去改變了中斷標識的值使得main方法中while循環的判斷不成立而跳出循環,因此main方法執行完畢以後線程就終止了。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是果斷地將線程立馬停止,因此這種終止線程的做法顯得更加安全和優雅。

Thread.interrupted

通過 interrupt方法可以設置一個標識告訴線程可以終止了,線程中還提供了靜態方Thread.interrupted()對設置中斷標識的線程復位。比如在上面的案例中,外面的線程調用 thread.interrupt 來設置中斷標識,而在線程裏面,又通過 Thread.interrupted 把線程的標識又進行了復位。實例代碼如下:

public class InterruptedDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(true){
                if(Thread.currentThread().isInterrupted()){
                    //默認情況下isInterrupted 返回 false、通過 thread.interrupt 變成了 true
                    System.out.println("復位之前:"+Thread.currentThread().isInterrupted());
                    //對線程進行復位,由 true 變成 false
                    Thread.interrupted();
                    System.out.println("復位之後:"+Thread.currentThread().isInterrupted());
                }
            }
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
    }
}

其他的線程復位

除了通過 Thread.interrupted 方法對線程中斷標識進行復位以外,還有一種被動復位的場景,就是拋出了一個InterruptedException 異常,在InterruptedException 拋出之前,JVM 會先把線程的中斷標識位清除,然後纔會拋出 InterruptedException,這個時候如果調用 isInterrupted 方法,將會返回 false,接下來分別通過下面兩個 demo 來進行演示:
Demo - 1:

public class InterruptDemoOne {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("線程執行");
            while(!Thread.currentThread().isInterrupted()){
                //默認情況下isInterrupted 返回 false、通過 thread.interrupt 變成了 true
                i++;
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
        System.out.println("未通過異常進行復位,線程是否終止:" + thread.isInterrupted());
    }
}

執行結果:
在這裏插入圖片描述
Demo - 2:

public class InterruptDemoTwo {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("線程執行");
            while(!Thread.currentThread().isInterrupted()){
                //默認情況下isInterrupted 返回 false、通過 thread.interrupt 變成了 true
                try {
                    Thread.sleep(1000);//睡眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
        System.out.println("通過異常進行復位,線程是否終止:" + thread.isInterrupted());
    }
}

執行結果:
在這裏插入圖片描述
注:當在sleep中的線程被調用interrupt方法時會放棄暫停的狀態,並拋InterruptedException異常

爲什麼要復位

Thread.interrupted()是屬於當前線程的,是當前線程對外界中斷信號的一個響應,表示自己已經得到了中斷信號,但不會立刻中斷自己,具體什麼時候中斷由自己決定,讓外界知道在自身中斷前,他的中斷狀態仍然是 false,這就是復位的原因。

線程終止的原理

首先來看一下interrupt()源碼到底幹了什麼:

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

	...
	
	private native void interrupt0();

我們看到調用 interrupt方法實際上是調用一個 native 方法interrupt0()來終止一個線程,首先 interrupt0()這個方法是在Thread 的靜態塊中來註冊的,跟start0()註冊的方式是一樣的,前面幾步的分析過程與啓動方法start()是一樣的,這裏就不重複了,直接看JVM_Interrupt的定義:

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");
  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

這個方法比較簡單,直接調用了 Thread::interrupt(thr)這個方法,這個方法的定義在Thread.cpp文件中,代碼如下:

void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

Thread::interrupt方法調用了os::interrupt方法,這個是調用平臺的interrupt方法,這個方法的實現是在 os_*.cpp文件中,其中星號代表的是不同平臺,因爲jvm是跨平臺的,所以對於不同的操作平臺,線程的調度方式都是不一樣的。我們以os_linux.cpp文件爲例:

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  //獲取本地線程對象
  OSThread* osthread = thread->osthread();
  if (!osthread->interrupted()) {//判斷本地線程對象是否爲中斷
    osthread->set_interrupted(true);//設置中斷狀態爲true
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    //這裏是內存屏障,內存屏障的目的是使得interrupted狀態對其他線程立即可見
    OrderAccess::fence();
    //_SleepEvent相當於Thread.sleep,表示如果線程調用了sleep方法,則通過unpark喚醒
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }
  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();
  //_ParkEvent用於synchronized同步塊和Object.wait(),這裏相當於也是通過unpark進行喚醒
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;
}

通過上面的代碼分析可以知道,thread.interrupt()方法實際就是設置一個interrupted狀態標識爲true、並且通過ParkEvent的unpark方法來喚醒線程。這裏要注意一下幾點:

  1. 對於synchronized阻塞的線程,被喚醒以後會繼續嘗試獲取鎖,如果失敗仍然可能被park。
  2. 在調用ParkEvent的park方法之前,會先判斷線程的中斷狀態,如果爲true,會清除當前線程的中斷標識。
  3. Object.wait、Thread.sleep、Thread.join會拋出InterruptedException。

InterruptedException出現的原因

這個異常的意思是表示一個阻塞被其他線程中斷了。然後,由於線程調用了interrupt()中斷方法,那麼Object.wait、Thread.sleep等被阻塞的線程被喚醒以後會通過is_interrupted方法判斷中斷標識的狀態變化,如果發現中斷標識爲true,則先清除中斷標識,然後拋出InterruptedException。需要注意的是,InterruptedException異常的拋出並不意味着線程必須終止,而是提醒當前線程有中斷的操作發生,至於接下來怎麼處理取決於線程本身,比如直接捕獲異常不做任何處理、將異常往外拋出、停止當前線程,並打印異常信息。爲了讓大家能夠更好的理解上面這段話,我們以Thread.sleep爲例直接從jdk的源碼中找到中斷標識的清除以及異常拋出的方法代碼。

找到 is_interrupted()方法,linux平臺中的實現在os_linux.cpp文件中,代碼如下:

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  OSThread* osthread = thread->osthread();
  bool interrupted = osthread->interrupted(); //獲取線程的中斷標識
  if (interrupted && clear_interrupted) {//如果中斷標識爲true
  	//設置中斷標識爲false,這也是爲什麼默認情況下isInterrupted 返回 false的原因
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }
  return interrupted;
}

然後在jvm.cpp文件中找到Thread.sleep這個操作在jdk中的源碼體現:

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");
  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //判斷並清除線程中斷狀態,如果中斷狀態爲true,拋出中斷異常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }
  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  JavaThreadSleepState jtss(thread);

我相信小夥伴們看到源碼中的註釋一定會豁然開朗,其實啓動原理與終止原理很相似,包括stop之類的其他一些Thread類中用native修飾的方法分析流程都是這樣。

結束語

**說心裏話,就單單一個線程我沒想到能寫到兩萬兩千多字,原本也只是想寫線程的一些基本概念和使用,但是有小夥伴還是想看看原理分析,所以趁這個機會我自己也是去找了很多資料和源碼去研究了下,發現一個小小的線程竟然會牽扯到這麼多東西,整體下來收穫還是挺大的,希望也可以給小夥伴們帶來收穫。一邊學一邊整理還是挺花時間的,併發系列不能做到每日一更,還望小夥伴們見諒,今天有事比較忙,本來寫不完了,但是想到已經答應小夥伴們周天前就可以看了,熬到凌晨三點半,也要寫完!任何原因都是不給自己懶惰理由!我會盡自己最大的努力,用最通俗易懂的方式進行總結,從0到1爭取讓每一個小夥伴都可以get到東西!

每個人可能會對同一個知識點的理解都不一樣,互相交流是一種很好的提升方式,應小夥伴要求,博主建了一個QQ羣(1062565785),有興趣的小夥伴可以入羣一起交流學習,羣裏也會不定時上傳一些有營養價值的學習、面試資料。這只是個學習交流羣,不是教育羣!不是教育羣!不是教育羣!重要的事情說三遍!進羣記得看公告哦!**

練習、用心、持續------致每一位追夢人!加油!!!

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