Thread

Thread概要.PNG

構(gòu)造函數(shù)

創(chuàng)建 Thread 對象的時間都會調(diào)用 init() 方法,取一個最常用的構(gòu)造方法

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        // 獲取當(dāng)前線程
        Thread parent = currentThread();
        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();
        this.group = g;

        this.target = target;
        this.priority = parent.getPriority();
        this.daemon = parent.isDaemon();
        setName(name);

        init2(parent);

        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        tid = nextThreadID();
    }    

init() 方法的第一個參數(shù)叫做線程組, Java 編程思想 書中提到,這個概念其實(shí)并沒有什么叼用. 在構(gòu)造方法中也可以看到傳入的是 null.

init() 方法開始就通過 currentThread() 獲取了創(chuàng)建 Thread 對象的當(dāng)前線程, 一般就是 main thread.

后續(xù)用 traget 保存了提交的任務(wù), 用 priority 保存了優(yōu)先級, 用 daemon 保存了是否是守護(hù)(后臺)線程. 其中必須要注意, prioritydaemon 的值取的是創(chuàng)建 Thread 對象的當(dāng)前進(jìn)程的相應(yīng)的值, 也就是繼承了當(dāng)前線程的相應(yīng)的屬性.

同時,還可以注意到, 在創(chuàng)建 Thread 對象時, 會默認(rèn)創(chuàng)建一個名字, 名字格式類似 Thread-threadId 這樣的形式.

執(zhí)行線程

在 Java 編程中, 一個 Thread 對象通常就是指的一個執(zhí)行線程(thread of execution), 然而這個說法并不確切, 因?yàn)橹挥姓{(diào)用了 Thread 對象的 start() 方法后, 才會創(chuàng)建執(zhí)行線程

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     */
    public synchronized void start() {
        // 線程只有啟動一次, 否則拋異常
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            // 用當(dāng)前的 Thread 對象創(chuàng)建執(zhí)行線程
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            // ...
        }
    }

可以看到, nativeCreate() 用當(dāng)前 Thread 對象創(chuàng)建了一個執(zhí)行線程. 這也就解釋了為何 Thread 對象調(diào)用 start() 方法后, Thread 對象沒有立即被回收, 而是等到執(zhí)行完任務(wù)后才會回收, 因?yàn)閳?zhí)行線程使用了 Thread 對象.

為何要傳入一個 Thread 對象? 因?yàn)閳?zhí)行線程要執(zhí)行一個任務(wù)(Runnable), 而 Thread 類正好實(shí)現(xiàn)了 Runbale 接口. 所以接下來就是調(diào)用 Thread 類的 run() 方法

當(dāng)執(zhí)行線程創(chuàng)建后, 會調(diào)用 Thread 對象的 run() 方法

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

從這里可以清楚看到, 如果創(chuàng)建 Thread 對象的時候, 不傳入 Runnable 對象, 這個線程其實(shí)不會做任何事. 另外一方面, 如果是繼承 Thread 類, 可以通過復(fù)寫 run() 方法來執(zhí)行任務(wù).

優(yōu)先級

創(chuàng)建 Thread 對象的時候, 會把當(dāng)前線程的優(yōu)先級賦予給 Thread 對象, 注意, 我說的是 Thread 對象, 不是線程. 真正設(shè)置線程優(yōu)先級的方法為 setPriority()

    public final void setPriority(int newPriority) {
        // ..
        if((g = getThreadGroup()) != null) {
            // ...
            synchronized(this) {
                // 賦值給 thread.priority
                this.priority = newPriority;
                if (isAlive()) {
                    // 給執(zhí)行線程設(shè)置優(yōu)先級
                    nativeSetPriority(newPriority);
                }
            }
        }
    }

nativeSetPriority() 才是真正的給執(zhí)行線程設(shè)置優(yōu)先級, 所以如果不調(diào)用 setPriority() 方法, 創(chuàng)建 Thread 對象的時候, 其實(shí)壓根就沒有把線程設(shè)置優(yōu)先級, 只是給 Thread 對象設(shè)置變量.

再來看看 getPriority() 方法

    public final int getPriority() {
        return priority;
    }

我驚訝的發(fā)現(xiàn), 這個獲取的優(yōu)先級居然不是執(zhí)行線程的優(yōu)先級, 而是 Thread 對象的優(yōu)先級, 百撕不得騎姐 ~

優(yōu)先級有三個, MIN_PRIORITY, NORM_PRIORITY, NORM_PRIORITY . 調(diào)度器會傾向于讓優(yōu)先級高的線程先執(zhí)行, 但是這并不意味著優(yōu)先級低的線程將得不到機(jī)會執(zhí)行(不然不就造成死鎖了). 優(yōu)先級應(yīng)該理解為執(zhí)行頻率. 所以試圖通過優(yōu)先級操作多線程的執(zhí)行順序, 通常就是錯誤的做法.

后臺線程

所謂后臺(daemon)線程, 也稱為守護(hù)線程, 是指在程序運(yùn)行的時候在后臺提供一種通用服務(wù)的線程, 并且這種線程并不屬于程序中不可或缺的部分. 因此當(dāng)所有非后臺線程執(zhí)行結(jié)束時, 程序也就終止了, 同時會殺死進(jìn)程中的所有后臺線程.

先看看 setDaemon() 方法

    /**
     * Marks this thread as either a {@linkplain #isDaemon daemon} thread
     * or a user thread. The Java Virtual Machine exits when the only
     * threads running are all daemon threads.
     *
     * This method must be invoked before the thread is started.
     */
    public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }

從注釋中可以明白兩點(diǎn)問題:

  1. 參數(shù) on 的值決定了線程是后臺線程還是用戶線程
  2. 如果只有后臺線程在運(yùn)行, JVM 將會退出, 也就是程序終止了.
  3. 這個方法必須在 Thread 對象調(diào)用 start() 方法之前調(diào)用. why? 可能是創(chuàng)建執(zhí)行線程的時候, 要用到這個屬性吧~

休眠

Thread 類的靜態(tài)方法 sleep() 會讓當(dāng)前的執(zhí)行線程在指定時間段內(nèi)休眠, 例如

        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

首先 Thread 對象調(diào)用 start() 方法會創(chuàng)建一個執(zhí)行線程, 然后執(zhí)行 Thread 對象的 run() 方法, 這樣 run() 方法就會新創(chuàng)建的執(zhí)行線程中執(zhí)行, 所以調(diào)用 Thread.Sleep(1000) 會讓線程休眠 1s.

再看看源碼

    public static void sleep(long millis) throws InterruptedException {
        Thread.sleep(millis, 0);
    }

    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        // STEP1: 判斷參數(shù)可用性
        if (millis < 0) {
            throw new IllegalArgumentException("millis < 0: " + millis);
        }
        if (nanos < 0) {
            throw new IllegalArgumentException("nanos < 0: " + nanos);
        }
        if (nanos > 999999) {
            throw new IllegalArgumentException("nanos > 999999: " + nanos);
        }

        // The JLS 3rd edition, section 17.9 says: "...sleep for zero
        // time...need not have observable effects."
        if (millis == 0 && nanos == 0) {
            // ...but we still have to handle being interrupted.
            if (Thread.interrupted()) {
              throw new InterruptedException();
            }
            return;
        }

        long start = System.nanoTime();
        long duration = (millis * NANOS_PER_MILLI) + nanos;

        // STEP2: 獲取當(dāng)前執(zhí)行線程的鎖
        Object lock = currentThread().lock;

        // Wait may return early, so loop until sleep duration passes.
        // STEP3: 同步鎖, 并執(zhí)行無限循環(huán)來休眠執(zhí)行線程, 直到休眠時間完畢
        synchronized (lock) {
            while (true) {
                sleep(lock, millis, nanos);

                long now = System.nanoTime();
                long elapsed = now - start;

                if (elapsed >= duration) {
                    break;
                }

                duration -= elapsed;
                start = now;
                millis = duration / NANOS_PER_MILLI;
                nanos = (int) (duration % NANOS_PER_MILLI);
            }
        }
    }

    @FastNative
    private static native void sleep(Object lock, long millis, int nanos)
        throws InterruptedException;     

直接看兩個參數(shù)的 sleep() 方法, 這個方法其實(shí)做了三件事:

  1. 判斷參數(shù)可用性, 否則會拋出異常
  2. 獲取當(dāng)前線程的鎖
  3. 同步鎖, 執(zhí)行循環(huán)來休眠線程,直到休眠時間完畢

第一步中會根據(jù)參數(shù)以及線程的中斷標(biāo)志, 會拋出參數(shù)異常(IllegalArgumentException)和中斷異常(InterruptedException). 但是我們經(jīng)常遇到的中斷異常是在無限循環(huán)的時候, 調(diào)用的 native sleep() 方法拋出的, 它是響應(yīng)線程中斷而拋出的異常, 那么會有兩個情況:

  1. 當(dāng)線程正在休眠的時間, 調(diào)用了 thread.interrupt() 方法, 會出現(xiàn)中斷異常
  2. 當(dāng)調(diào)用了 thread.interrupt() 方法后, 再試圖進(jìn)入休眠, 也會出現(xiàn)中斷異常

從第三步中還可以看出,休眠期間是不會釋放當(dāng)前 thread 的鎖的, 這在并發(fā)資源競爭中很關(guān)鍵。

讓步

    /**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();

注釋說明了幾點(diǎn)問題:

  1. yield() 方法只是暗示調(diào)度器,當(dāng)前線程可以讓出處理器,調(diào)度器也可以忽略這個暗示. 所以可以使用,并不能依賴它的效果
  2. 一般用于調(diào)度或者測試,可以用來復(fù)現(xiàn)問題。

加入線程

        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("execute t1");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1 awaken");
            }
        });
        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("execute t2");
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 awaken");
            }
        };
        t1.start();
        t2.start();

幾乎同時啟動了兩個線程 t1 和 t2。 然而在 t2 執(zhí)行的時候, 突然調(diào)用 t1.join() 讓 t1 執(zhí)行, t1 執(zhí)行完畢后, t2 再繼續(xù)。

執(zhí)行結(jié)果如下

01-17 18:03:16.799  System.out: execute t1
01-17 18:03:16.799  System.out: execute t2
01-17 18:03:21.800  System.out: t1 awaken
01-17 18:03:21.800  System.out: t2 awaken

從 Log 可以看出, t2 確實(shí)等待 t1 休眠了5后,t2 才執(zhí)行后面的代碼。

看看源碼

    public final void join() throws InterruptedException {
        join(0);
    }

    /**
     * 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.
     */
    public final void join(long millis) throws InterruptedException {
        synchronized (lock) {
            // ...
            if (millis == 0) {
                while (isAlive()) {
                    lock.wait(0);
                }
            } else {
                // ...
            }
        }
    }

代碼我做了省略,主要邏輯就是這樣。 這里理解起來有點(diǎn)難,我可以把例子中代碼改編下

        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("execute t2");
                try {
                    synchronized (t1.lock) {
                        // ...
                        if (millis == 0) {
                            while (t1.isAlive()) {
                                t1.lock.wait(0);
                            }
                        } else {
                            // ...
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 awaken");
            }
        };

這樣看起來就舒服了, 首先通過 synchronized 獲取 t1.lock 的鎖,防止 t1 的并發(fā)問題。 然后,調(diào)用 t1.isAlive() 判斷 t1 是否還存活著, 如果還存活著,就調(diào)用 t1.lock.wait(0) 來掛起 t2 線程,注意,是掛起 t2 線程, 不是 t1。 當(dāng) t1 執(zhí)行完畢后,會調(diào)用 t1.lock.notifyAll() 方法來喚醒 t2 線程,從而讓 t2 線程繼續(xù)執(zhí)行。

中斷

前面提到過,執(zhí)行線程休眠或準(zhǔn)備休眠的時候,如果調(diào)用 thread.interrupte() 方法會產(chǎn)生中斷異常(InterruptedException).

看看 interrupte() 源碼

    /**
     * Interrupts this thread.
     *
     * <p> Unless the current thread is interrupting itself, which is
     * always permitted, the {@link #checkAccess() checkAccess} method
     * of this thread is invoked, which may cause a {@link
     * SecurityException} to be thrown.
     *
     * <p> If this thread is blocked in an invocation of the {@link
     * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
     * Object#wait(long, int) wait(long, int)} methods of the {@link Object}
     * class, or of the {@link #join()}, {@link #join(long)}, {@link
     * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
     * methods of this class, then its interrupt status will be cleared and it
     * will receive an {@link InterruptedException}.
     *
     * <p> If this thread is blocked in an I/O operation upon an {@link
     * java.nio.channels.InterruptibleChannel InterruptibleChannel}
     * then the channel will be closed, the thread's interrupt
     * status will be set, and the thread will receive a {@link
     * java.nio.channels.ClosedByInterruptException}.
     *
     * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
     * then the thread's interrupt status will be set and it will return
     * immediately from the selection operation, possibly with a non-zero
     * value, just as if the selector's {@link
     * java.nio.channels.Selector#wakeup wakeup} method were invoked.
     *
     * <p> If none of the previous conditions hold then this thread's interrupt
     * status will be set. </p>
     *
     * <p> Interrupting a thread that is not alive need not have any effect.
     */
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                nativeInterrupt();
                b.interrupt(this);
                return;
            }
        }
        nativeInterrupt();
    }

從注釋中總結(jié)以下幾點(diǎn)

  1. Objectwait() 方法,Threadsleep() 方法和 join() 方法,會導(dǎo)致線程阻塞。 如果調(diào)用了 interrupt() 方法,線程的中斷標(biāo)志(interrupt status)將會被清除,線程也會收到 InterruptedException。
  2. InterruptibleChannel 也會阻塞線程,如果調(diào)用了 interrupt() 方法, 通道會被關(guān)閉,線程的中斷標(biāo)志將被設(shè)置,并且線程會收到 ClosedByInterruptException
  3. Selector 也會阻塞線程,當(dāng)調(diào)用了 interrupt() 方法后,線程的中斷標(biāo)志將被設(shè)置,并且會立即從 selection operation 中返回,并且可以帶有一個非零的值。
  4. 如果沒有以上的任何一個條件,那么只會設(shè)置中斷標(biāo)志。

注釋中已經(jīng)列舉了所有產(chǎn)生中斷異常的情況,如果不是這種情況,肯定就不會產(chǎn)生中斷異常,例如 I/O 造成的阻塞,synchronized 無法獲取對象鎖造成的阻塞, 都是不會產(chǎn)生中斷異常的。 其實(shí),有個小技巧,用 IDE 寫代碼的時候,如果提示要 catch 中斷異常,那么相應(yīng)的代碼就會響應(yīng)中斷異常,如果沒有提示,肯定就不響應(yīng)了。

從上面的四點(diǎn)總結(jié),還需要注意一個問題,就是中斷標(biāo)志

  1. 如果產(chǎn)生中斷異常,線程退出,會清除中斷標(biāo)志
  2. 如果沒有產(chǎn)生中斷異常,就會設(shè)置中斷標(biāo)志

Thread 類還一個靜態(tài)的 interrupted() 方法

    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    @FastNative
    public static native boolean interrupted();

Thread.interrupte() 方法比較有意思,它會檢測當(dāng)前線程是否已經(jīng)被中斷過,并且會清楚中斷標(biāo)志。 前面說過,當(dāng)調(diào)用 thread.interrupte() 的時候, 如果沒有碰到能產(chǎn)生中斷的情況, 線程是不會拋出中斷異常的, 那么我們可以用 Thread.interrupted() 方法檢測中斷標(biāo)志,然后手動退出。

當(dāng)然檢測中斷標(biāo)志還有一個方法

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */
    @FastNative
    public native boolean isInterrupted();

這個方法并不是靜態(tài)方法,它與靜態(tài)的 interrupted() 方法的唯一區(qū)別就是,不清除當(dāng)前線程的中斷標(biāo)志。

互斥鎖中斷

interrupte() 方法的注釋還有一點(diǎn)沒提及, 雖然 synchronized 方法或者臨界區(qū)的阻塞不可中斷 ,但是 ReentrantLock 上阻塞具備可以中斷的能力。

    ReentrantLock lock = new ReentrantLock();
    try {
        mLock.lockInterruptibly();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

從拋出的異常就可以看到,ReentrantLock 產(chǎn)生的阻塞具備可中斷的能力。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容