Thread執行緒知識點講解

神易風發表於2022-03-29
本文出處Thread執行緒知識點講解 轉載請說明出處

內部屬性

//執行緒名,如果建立時沒有指定則使用Thread- + 建立序列號
private volatile String name;
   //執行緒優先順序  Java只是給作業系統一個優先順序的參考值,執行緒最終在作業系統的優先順序是多少還是由作業系統決定。
    private int priority;

    //守護執行緒 
    private boolean daemon = false;

    //為JVM保留欄位
    private boolean stillborn = false;
    private long eetop;

    /* What will be run. */
    private Runnable target;

    //執行緒組,每一個執行緒必定存於一個執行緒組中,執行緒不能獨立於執行緒組外
    private ThreadGroup group;

    // 類載入器,當執行緒需要載入類時,會使用內部類加器
    private ClassLoader contextClassLoader;

    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    /*
     * The requested stack size for this thread, or 0 if the creator did
     * not specify a stack size.  It is up to the VM to do whatever it
     * likes with this number; some VMs will ignore it.
     */
    private final long stackSize;

    /*
     * JVM-private state that persists after native thread termination.
     */
    private long nativeParkEventPointer;

    /*
     * Thread ID
     */
    private final long tid;

    /* For generating thread ID */
    private static long threadSeqNumber;

    // 這個執行緒號是整個Thread 類共享的
    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }

    /*
     * 執行緒狀態
     */
    private volatile int threadStatus;

建構函式

    public Thread() {
        this(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        this(group, target, name, stackSize, null, true);
    }

    private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread(); //從建立Thread 的執行緒中獲取到父執行緒
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security manager doesn't have a strong opinion
               on the matter, use the parent thread group. */
            if (g == null) { //沒有設定執行緒組則使用當前執行緒的執行緒組
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(
                        SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        //對沒有啟動執行緒進行計數
        g.addUnstarted();

        this.group = g;
        //如果在建立執行緒時沒有設定守護執行緒,優先順序、類加器這些,全部都是當前現場的
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        this.tid = nextThreadID();
    }

構造方法其實都是對Thread 內部屬性進行初始化,比如執行緒名、優先順序、類加器、執行緒Id。如果沒有設定這些屬性全部繼承自當前的。讓我比較奇怪是非常重要的threadStatus 沒有賦值,而是使用了預設值,我猜想這個變數全程都是由c++來變更的,所以不必要使用Java進行賦值。
已經初始化的執行緒物件可以通過set方法去修改守護執行緒、執行緒名、優先順序。

執行緒狀態

 public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called {@code Object.wait()}
         * on an object is waiting for another thread to call
         * {@code Object.notify()} or {@code Object.notifyAll()} on
         * that object. A thread that has called {@code Thread.join()}
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

執行緒狀態經常被問於面試中,幾個狀態和代表涵義大家都有記一記。

狀態描述場景
NEWThread執行緒剛剛被建立,建立狀態new Thread
RUNNABLE執行狀態,執行緒正在執行中Thread.start
BLOCKED堵塞狀態synchronized 競爭失敗
WAITING等待,這種狀態要麼無限等待下去,要麼被喚醒Object.wait、Lock
TIMED_WAITING等待超時,在等待時設定了時間,到時會自動喚醒Thread.sleep、LockSupport.parkNanos
TERMINATED死亡狀態執行緒已經執行完任務

從下圖可以發現從建立-> 執行-> 死亡 這個過程是不可逆的。
image.png

執行緒執行和停止

    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)  //狀態必須是建立狀態  NEW  ,防止一個物件多次呼叫start 方法
            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);  //加入執行緒組容器中,未開始執行緒數-1  

        boolean started = false;
        try {
            start0();  
            started = true;
        } finally {
            try {
                 // 進入到這裡,則start0 建立一個執行緒失敗了,要從執行緒組中刪除它,未開始執行緒再加回來
                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方法比較簡單的,先判斷狀態是否正確,在建立之前加入到執行緒組裡面,失敗了再移除。start0 方法應該就是呼叫系統資源真正去建立一個執行緒了,而且執行緒狀態也是由這個方法修改的。

run方法只有使用Thread來建立執行緒,並且使用Runnable傳參才會執行這裡run方法,繼承方式應該是直接呼叫子類run方法了。

    public void run() {
        if (target != null) {  //有傳入Runnable 物件,則呼叫該物件實現run方法
            target.run();
        }
    }

stop方法雖然在Java2已經被官方停用了,很值得去了解下的。

    @Deprecated(since="1.2")
    public final void stop() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }
        // A zero status value corresponds to "NEW", it can't change to
        // not-NEW because we hold the lock.
        if (threadStatus != 0) { //不是NEW,執行緒已經執行了,如果被掛起了,需要對它進行喚醒
            resume(); // Wake up thread if it was suspended; no-op otherwise
        }

        // The VM can handle all thread states
        stop0(new ThreadDeath()); //停止執行緒,並且丟擲一個異常給JVM
    }

    private native void stop0(Object o);

看完這個方法,也沒有看出來stop()能幹什麼,我也不是很清楚這個stop能幹什麼,我將寫幾個例子驗證功能。
建立幾個執行緒去執行下任務,執行一會後,對所有執行緒呼叫stop方法,是否會退出任務。

public class ThreadStopTest {

    public static void main(String[] args) {
        ThreadStopTest t = new ThreadStopTest();
        Runnable r = () -> {
          int i = 0;
          while (i < 1000){
              t.spinMills(500);
              System.out.println(Thread.currentThread().getName() + " : " + i);
              i++;
          }
        };

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
        t1.start();
        t2.start();
        t3.start();
        t.spinMills(2000);
        t1.stop();
        t2.stop();
        t3.stop();
    }

    public void spinMills(long millisecond){
       long start = System.currentTimeMillis();
       while (System.currentTimeMillis() - start < millisecond){
           //自旋 ,模擬執行任務
       }
    }
}

執行結果

Thread-1 : 0
Thread-0 : 0
Thread-2 : 0
Thread-1 : 1
Thread-0 : 1
Thread-2 : 1
Thread-2 : 2
Thread-1 : 2
Thread-0 : 2

呼叫完stop方法,執行緒立刻退出任務,連一個異常都沒有丟擲的,真的是非常乾脆。如果有人不下心使用stop方法,出現問題都非常難排除,所以Java 官方早早就停止使用它了,詳細看官方說明

如果想優雅停止一個正在執行的執行緒,官方建議使用interrupted()。執行緒中斷就是目標執行緒傳送一箇中斷訊號,能夠收到中斷訊號執行緒自己實現退出邏輯。簡單點說就是執行緒A在幹活,突然有個人對它做了一個動作,執行緒A在知道這個動作涵義,它會知道自己要停下來。說白這就一個動作,如果執行緒邏輯沒有處理這個動作程式碼,執行緒並不會退出的。看下Thread類裡面有那些方法。

方法備註
interrupt()中斷目標執行緒,給目標執行緒發一箇中斷訊號,執行緒被打上中斷標記
isInterrupted()判斷目標執行緒是否被中斷,不會清除中斷標記
interrupted判斷目標執行緒是否被中斷,會清除中斷標記

實現一個簡單例子

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            while (!Thread.currentThread().isInterrupted()){
                //do some
                System.out.println(System.currentTimeMillis());
            }
            System.out.println("執行緒準備退出啦");
            Thread.interrupted();
        };
        Thread t = new Thread(r);
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }

上面程式碼核心是中斷狀態,如果中斷被清除了,那程式不會跳出while迴圈的,下面改一下,新增一個sleep方法

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            while (!Thread.currentThread().isInterrupted()){
                //do some
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis());
            }
            System.out.println("執行緒準備退出啦");
            Thread.interrupted();
        };
        Thread t = new Thread(r);
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }

執行結果 : 傳送中斷後,Thread.sleep直接丟擲一個異常,並不會跳出迴圈。
因為sleep會響應中斷,丟擲一箇中斷異常,再清除執行緒中斷狀態。再回到while 判斷時,中斷狀態已經被清除了,繼續迴圈下去。
sleep()是一個靜態native 方法,使當前執行的執行緒休眠指定時間,但是休眠的執行緒不會放棄監控器的鎖(synchronized),當任何執行緒要中斷當前執行緒時,會丟擲InterruptedException異常,並且清理當前執行緒的中斷狀態。所以在方法呼叫上就會丟擲這個異常,讓呼叫者去處理中斷異常。

join和yield方法

join()就是一個等待方法,等待當前執行緒任務執行後,再次喚醒被呼叫的執行緒,常常用來控制多執行緒任務執行順序。

    /**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {  //這裡獲取執行緒狀態,只是不是開始和死亡就算alive了
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0); //在指定時間內沉睡
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }

想了解方法主要看方法註釋就行,在指定時間內等待被呼叫者的執行緒死亡,如果沒有死亡時間到了會自行喚醒,如果時間為0則永遠等待下去,直到執行執行緒執行完任務。喚醒是由notifyAll執行的,但是沒看見在哪裡執行這個方法。查了一下資料知道每個執行緒執行完成後都會呼叫exit()方法,在exit會呼叫notifyAll。
yield(): 單詞翻譯過來就是讓步的意思。主要作用當執行緒獲取到執行權時,呼叫這個方法會主動讓出執行器,它跟上面wait、sleep 不同,執行緒狀態是沒有改變的,此時任然是RUN。比如一個執行緒獲取鎖失敗了,這時執行緒什麼不能幹,獲取鎖本身是很快,此時將執行緒掛起了,有點得不償失,不如此時讓出CPU執行器,讓其他執行緒去執行。既不會浪費CPU寶貴時間,也不需要太耗費效能。這個方法經常用於java.util.concurrent.locks包下同步方法,看過併發工具類的同學應該都認識它。

執行緒間協作

wait方法讓當前執行緒進入等待狀態(WAITING),並且釋放監控鎖,只有當其他執行緒呼叫notify或者notifyAll才會喚醒執行緒。
notify喚醒一個在等待狀態的執行緒,重新進入RUNNABLE狀態。
notifyAll喚醒所有正在等待狀態的執行緒,重新進入RUNNABLE狀態。
上面三個方法都必須在監控鎖(synchronized)下使用,不然會丟擲IllegalMonitorStateException。
wait、notify 兩個方法結合就可以實現執行緒之間協作。比如最經典的生產者-消費者模型: 當上遊消費者傳送傳送資訊太多,導致佇列擠壓已經滿了,這時消費者這邊可以使用wait,讓生產者停下里,當消費者已經開始消費了,此時佇列已經被消費走一個資訊了,有空間了,消費者可以呼叫notify,讓上游生產者繼續運作起來。當佇列裡面資訊已經被消費完時,消費者會呼叫wait,讓執行緒進入等待中,當上遊執行緒有資訊傳送到佇列時,此時佇列中資訊就不是全空的了,就可以呼叫wait 喚醒一個等待消費者。這樣就可以形成執行緒之間相互通訊的效果了。
簡單實現消費者-生產者模型

    public void push(T t){
        synchronized (lock){
            size++;
            if (size == QUEUE_CAPACTIY) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.notify();
            //入佇列中
        }
    }

    public T poll(){
        synchronized (lock){
            size--;
            if (size == 0) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.notify();
            return T;
        }
    }

Callable和 Thread關係

我們知道了所有的執行緒其實都是Thread.start去建立的,重寫run 方法達到異常執行任務,但是Callable這個介面是否也是使用Thread或者Runnable介面,主要看FutureTask就知道如何實現了。
看下run方法

    public void run() {
           //如果執行緒已經被建立了,則不需要再次執行任務了
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))  
            return;
        try {
            Callable<V> c = callable;  //callable 方法實現類
            if (c != null && state == NEW) { //剛剛初始化的狀態
                V result;
                boolean ran;
                try {
                    result = c.call(); //執行任務
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex); //儲存異常,將等待佇列的執行緒全部喚醒過來
                }
                if (ran)
                    set(result); //儲存執行結果,將等待佇列的執行緒全部喚醒過來
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

可以看出Callable仍然是使用Thread來建立執行緒的,內部通過維護state來判斷任務狀態,在run 方法中執行call方法,儲存異常和執行結果。
看下get() 如何獲取執行結果的吧

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)  //還在執行中
            s = awaitDone(false, 0L);  //等待任務執行完成或者中斷,會堵塞呼叫執行緒
        return report(s);
    }

   /**
     * Awaits completion or aborts on interrupt or timeout.
     *
     * @param timed true if use timed waits
     * @param nanos time to wait, if timed
     * @return state upon completion or at timeout
     */
    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        // The code below is very delicate, to achieve these goals:
        // - call nanoTime exactly once for each call to park
        // - if nanos <= 0L, return promptly without allocation or nanoTime
        // - if nanos == Long.MIN_VALUE, don't underflow
        // - if nanos == Long.MAX_VALUE, and nanoTime is non-monotonic
        //   and we suffer a spurious wakeup, we will do no worse than
        //   to park-spin for a while
        long startTime = 0L;    // Special value 0L means not yet parked
        WaitNode q = null;
        boolean queued = false;
        for (;;) { 
            int s = state;
            if (s > COMPLETING) { //如果狀態已經有執行中變成其他 ,直接將狀態返回
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) //正在執行中,讓出CPU執行權,而不是變換執行緒狀態
                // We may have already promised (via isDone) that we are done
                // so never return empty-handed or throw InterruptedException
                Thread.yield();
            else if (Thread.interrupted()) { //處理執行緒中斷,退出自旋
                removeWaiter(q);  //刪除佇列中的執行緒
                throw new InterruptedException();
            }
            else if (q == null) {
                if (timed && nanos <= 0L)
                    return s;
                q = new WaitNode();
            }
            else if (!queued)  //將等待結果執行緒放入一個佇列中,其實這個佇列就是來處理等待結果執行緒的中斷的
                queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
            else if (timed) {
                final long parkNanos;
                if (startTime == 0L) { // first time
                    startTime = System.nanoTime();
                    if (startTime == 0L)
                        startTime = 1L;
                    parkNanos = nanos;
                } else {
                    long elapsed = System.nanoTime() - startTime;
                    if (elapsed >= nanos) {
                        removeWaiter(q);
                        return state;
                    }
                    parkNanos = nanos - elapsed;
                }
                // nanoTime may be slow; recheck before parking
                if (state < COMPLETING) //任務沒有啟動,掛起等待執行緒
                    LockSupport.parkNanos(this, parkNanos);
            }
            else
                LockSupport.park(this); //任務沒有開始,掛起呼叫者,任務完成後會將它喚醒的
        }
    }

現在基本就明瞭,使用run 呼叫call方法,將執行結果儲存起來,然後get 方法這邊使用自旋方法等待執行結果,並且使用佇列將等待的執行緒儲存起來,來處理執行緒的喚醒、中斷。

總結

這裡簡單說了Thread的構造方法,屬性設定,比較重要就是執行緒幾個狀態,狀態流轉、執行緒啟動停止,中斷處理,幾個常用方法的介紹。簡單說了下FutureTask實現原理,結合上面提到的知識點,上面提到這些知識都是挺重要的,你可以看到大部分Java併發類都用到這些知識來開發的,頻繁出現在面試中也是可以理解的。

相關文章