從原始碼的角度再學「Thread」

張少林同學發表於2019-01-19

從原始碼的角度再學「Thread」

前言

Java中的執行緒是使用Thread類實現的,Thread在初學Java的時候就學過了,也在實踐中用過,不過一直沒從原始碼的角度去看過它的實現,今天從原始碼的角度出發,再次學習Java Thread,願此後對Thread的實踐更加得心應手。

從註釋開始

相信閱讀過JDK原始碼的同學都能感受到JDK原始碼中有非常詳盡的註釋,閱讀某個類的原始碼應當先看看註釋對它的介紹,註釋原文就不貼了,以下是我對它的總結:

  • Thread是程式中執行的執行緒,Java虛擬機器允許應用程式同時允許多個執行執行緒

  • 每個執行緒都有優先順序的概念,具有較高優先順序的執行緒優先於優先順序較低的執行緒執行

  • 每個執行緒都可以被設定為守護執行緒

  • 當在某個執行緒中執行的程式碼建立一個新的Thread物件時,新的執行緒優先順序跟建立執行緒一致

  • Java虛擬機器啟動的時候都會啟動一個叫做main的執行緒,它沒有守護執行緒,main執行緒會繼續執行,直到以下情況傳送

    • Runtime 類的退出方法exit被呼叫並且安全管理器允許進行退出操作
    • 所有非守護執行緒均已死亡,或者run方法執行結束正常返回結果,或者run方法丟擲異常
  • 建立執行緒第一種方式:繼承Thread類,重寫run方法

    //定義執行緒類
    class PrimeThread extends Thread {
          long minPrime;
          PrimeThread(long minPrime) {
              this.minPrime = minPrime;
          }
          public void run() {
              // compute primes larger than minPrime
               . . .
          }
      }
    //啟動執行緒
    PrimeThread p = new PrimeThread(143);
    p.start();
    複製程式碼
  • 建立執行緒第二種方式:實現Runnable介面,重寫run方法,因為Java的單繼承限制,通常使用這種方式建立執行緒更加靈活

    //定義執行緒
     class PrimeRun implements Runnable {
          long minPrime;
          PrimeRun(long minPrime) {
              this.minPrime = minPrime;
          }
          public void run() {
              // compute primes larger than minPrime
               . . .
          }
      }
    //啟動執行緒
    PrimeRun p = new PrimeRun(143);
    new Thread(p).start();
    複製程式碼
  • 建立執行緒時可以給執行緒指定名字,如果沒有指定,會自動為它生成名字

  • 除非另有說明,否則將null引數傳遞給Thread類中的建構函式或方法將導致丟擲 NullPointerException

Thread 常用屬性

閱讀一個Java類,先從它擁有哪些屬性入手:

//執行緒名稱,建立執行緒時可以指定執行緒的名稱
private volatile String name;

//執行緒優先順序,可以設定執行緒的優先順序
private int priority;

//可以配置執行緒是否為守護執行緒,預設為false
private boolean daemon = false;

//最終執行執行緒任務的`Runnable`
private Runnable target;

//描述執行緒組的類
private ThreadGroup group;

//此執行緒的上下文ClassLoader
private ClassLoader contextClassLoader;

//所有初始化執行緒的數目,用於自動編號匿名執行緒,當沒有指定執行緒名稱時,會自動為其編號
private static int threadInitNumber;

//此執行緒請求的堆疊大小,如果建立者沒有指定堆疊大小,則為0。, 虛擬機器可以用這個數字做任何喜歡的事情。, 一些虛擬機器會忽略它。
private long stackSize;

//執行緒id
private long tid;

//用於生成執行緒ID
private static long threadSeqNumber;

//執行緒狀態
private volatile int threadStatus = 0;

//執行緒可以擁有的最低優先順序
public final static int MIN_PRIORITY = 1;

//分配給執行緒的預設優先順序。
public final static int NORM_PRIORITY = 5;

//執行緒可以擁有的最大優先順序
public final static int MAX_PRIORITY = 10;
複製程式碼

所有的屬性命名都很語義化,其實已看名稱基本就猜到它是幹嘛的了,難度不大~~

Thread 構造方法

瞭解了屬性之後,看看Thread例項是怎麼構造的?先預覽下它大致有多少個構造方法:

從原始碼的角度再學「Thread」

檢視每個構造方法內部原始碼,發現均呼叫的是名為init的私有方法,再看init方法有兩個過載,而其核心方法如下:

   /**
     * Initializes a Thread.
     *
     * @param g                   執行緒組
     * @param target              最終執行任務的 `run()` 方法的物件
     * @param name                新執行緒的名稱
     * @param stackSize           新執行緒所需的堆疊大小,或者 0 表示要忽略此引數
     * @param acc                 要繼承的AccessControlContext,如果為null,則為 AccessController.getContext()
     * @param inheritThreadLocals 如果為 true,從構造執行緒繼承可繼承的執行緒區域性的初始值
     */
    private void init(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();
        //獲取系統安全管理器
        SecurityManager security = System.getSecurityManager();
        //如果執行緒組為空
        if (g == null) {
            //如果安全管理器不為空
            if (security != null) {
                //獲取SecurityManager中的執行緒組
                g = security.getThreadGroup();
            }
            //如果獲取的執行緒組還是為空
            if (g == null) {
                //則使用父執行緒的執行緒組
                g = parent.getThreadGroup();
            }
        }
        
        //檢查安全許可權
        g.checkAccess();

        //使用安全管理器檢查是否有許可權
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        
        //執行緒組中標記未啟動的執行緒數+1,這裡方法是同步的,防止出現執行緒安全問題
        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);
        //初始化當前執行緒物件的堆疊大小
        this.stackSize = stackSize;

        //初始化當前執行緒物件的執行緒ID,該方法是同步的,內部實際上是threadSeqNumber++
        tid = nextThreadID();
    }
複製程式碼

另一個過載init私有方法如下,實際上內部呼叫的是上述init方法:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
複製程式碼

接下來看看所有構造方法:

  1. 空構造方法

     public Thread() {
            init(null, null, "Thread-" + nextThreadNum(), 0);
        }
    複製程式碼

    內部呼叫的是init第二個過載方法,引數基本都是預設值,執行緒名稱寫死為"Thread-" + nextThreadNum()格式,nextThreadNum()為一個同步方法,內部維護一個靜態屬性表示執行緒的初始化數量+1:

     private static int threadInitNumber;
        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        }
    複製程式碼
  2. 自定義執行任務Runnable物件的構造方法

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    複製程式碼

    與第一個構造方法區別在於可以自定義Runnable物件

  3. 自定義執行任務Runnable物件和AccessControlContext物件的構造方法

     Thread(Runnable target, AccessControlContext acc) {
        init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
    }
    複製程式碼
  4. 自定義執行緒組ThreadGroup和執行任務Runnable物件的構造方法

    public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }
    複製程式碼
  5. 自定義執行緒名稱name的構造方法

     public Thread(String name) {
        init(null, null, name, 0);
    }
    複製程式碼
  6. 自定義執行緒組ThreadGroup和執行緒名稱name的構造方法

     public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }
    複製程式碼
  7. 自定義執行任務Runnable物件和執行緒名稱name的構造方法

     public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }
    複製程式碼
  8. 自定義執行緒組ThreadGroup和執行緒名稱name和執行任務Runnable物件的構造方法

      public Thread(ThreadGroup group, Runnable target, String name) {
        init(group, target, name, 0);
    }
    複製程式碼
  9. 全部屬性都是自定義的構造方法

      public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }
    複製程式碼

Thread提供了非常靈活的過載構造方法,方便開發者自定義各種引數的Thread物件。

常用方法

這裡記錄一些比較常見的方法吧,對於Thread中存在的一些本地方法,我們暫且不用管它~

設定執行緒名稱

設定執行緒名稱,該方法為同步方法,為了防止出現執行緒安全問題,可以手動呼叫Thread的例項方法設定名稱,也可以在構造Thread時在構造方法中傳入執行緒名稱,我們通常都是在構造引數時設定

   public final synchronized void setName(String name) {
         //檢查安全許可權
          checkAccess();
         //如果形參為空,丟擲空指標異常
          if (name == null) {
              throw new NullPointerException("name cannot be null");
          }
  	  //給當前執行緒物件設定名稱
          this.name = name;
          if (threadStatus != 0) {
              setNativeName(name);
          }
      }
複製程式碼

獲取執行緒名稱

內部直接返回當前執行緒物件的名稱屬性

  public final String getName() {
        return name;
    }
複製程式碼

啟動執行緒

public synchronized void start() {
        //如果不是剛建立的執行緒,丟擲異常
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        //通知執行緒組,當前執行緒即將啟動,執行緒組當前啟動執行緒數+1,未啟動執行緒數-1
        group.add(this);
        
        //啟動標識
        boolean started = false;
        try {
            //直接呼叫本地方法啟動執行緒
            start0();
            //設定啟動標識為啟動成功
            started = true;
        } finally {
            try {
                //如果啟動呢失敗
                if (!started) {
                    //執行緒組內部移除當前啟動的執行緒數量-1,同時啟動失敗的執行緒數量+1
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
複製程式碼

我們正常的啟動執行緒都是呼叫Threadstart()方法,然後Java虛擬機器內部會去呼叫Thredrun方法,可以看到Thread類也是實現Runnable介面,重寫了run方法的:

 @Override
    public void run() {
        //當前執行任務的Runnable物件不為空,則呼叫其run方法
        if (target != null) {
            target.run();
        }
    }
複製程式碼

Thread的兩種使用方式:

  • 繼承Thread類,重寫run方法,那麼此時是直接執行run方法的邏輯,不會使用target.run();
  • 實現Runnable介面,重寫run方法,因為Java的單繼承限制,通常使用這種方式建立執行緒更加靈活,這裡真正的執行邏輯就會交給自定義Runnable去實現

設定守護執行緒

本質操作是設定daemon屬性

public final void setDaemon(boolean on) {
        //檢查是否有安全許可權
        checkAccess();
        //本地方法,測試此執行緒是否存活。, 如果一個執行緒已經啟動並且尚未死亡,則該執行緒處於活動狀態
        if (isAlive()) {
            //如果執行緒先啟動後再設定守護執行緒,將丟擲異常
            throw new IllegalThreadStateException();
        }
        //設定當前守護執行緒屬性
        daemon = on;
    }
複製程式碼

判斷執行緒是否為守護執行緒

 public final boolean isDaemon() {
        //直接返回當前物件的守護執行緒屬性
        return daemon;
    }
複製程式碼

執行緒狀態

先來個執行緒狀態圖:

從原始碼的角度再學「Thread」

獲取執行緒狀態:

 public State getState() {
        //由虛擬機器實現,獲取當前執行緒的狀態
        return sun.misc.VM.toThreadState(threadStatus);
    }
複製程式碼

執行緒狀態主要由內部列舉類State組成:

  public enum State {
      
        NEW,

      
        RUNNABLE,

      
        BLOCKED,

       
        WAITING,

       
        TIMED_WAITING,

       
        TERMINATED;
    }
複製程式碼
  • NEW:剛剛建立,尚未啟動的執行緒處於此狀態
  • RUNNABLE:在Java虛擬機器中執行的執行緒處於此狀態
  • BLOCKED:被阻塞等待監視器鎖的執行緒處於此狀態,比如執行緒在執行過程中遇到synchronized同步塊,就會進入此狀態,此時執行緒暫停執行,直到獲得請求的鎖
  • WAITING:無限期等待另一個執行緒執行特定操作的執行緒處於此狀態
    • 通過 wait() 方法等待的執行緒在等待 notify() 方法
    • 通過 join() 方法等待的執行緒則會等待目標執行緒的終止
  • TIMED_WAITING:正在等待另一個執行緒執行動作,直到指定等待時間的執行緒處於此狀態
    • 通過 wait() 方法,攜帶超時時間,等待的執行緒在等待 notify() 方法
    • 通過 join() 方法,攜帶超時時間,等待的執行緒則會等待目標執行緒的終止
  • TERMINATED:已退出的執行緒處於此狀態,此時執行緒無法再回到 RUNNABLE 狀態

執行緒休眠

這是一個靜態的本地方法,使當前執行的執行緒休眠暫停執行 millis 毫秒,當休眠被中斷時會丟擲InterruptedException中斷異常

    /**
     * Causes the currently executing thread to sleep (temporarily cease
     * execution) for the specified number of milliseconds, subject to
     * the precision and accuracy of system timers and schedulers. The thread
     * does not lose ownership of any monitors.
     *
     * @param  millis
     *         the length of time to sleep 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 static native void sleep(long millis) throws InterruptedException;
複製程式碼

檢查執行緒是否存活

本地方法,測試此執行緒是否存活。 如果一個執行緒已經啟動並且尚未死亡,則該執行緒處於活動狀態。

    /**
     * Tests if this thread is alive. A thread is alive if it has
     * been started and has not yet died.
     *
     * @return  <code>true</code> if this thread is alive;
     *          <code>false</code> otherwise.
     */
    public final native boolean isAlive();
複製程式碼

執行緒優先順序

  • 設定執行緒優先順序
    /**
     * Changes the priority of this thread.
     * <p>
     * First the <code>checkAccess</code> method of this thread is called
     * with no arguments. This may result in throwing a
     * <code>SecurityException</code>.
     * <p>
     * Otherwise, the priority of this thread is set to the smaller of
     * the specified <code>newPriority</code> and the maximum permitted
     * priority of the thread's thread group.
     *
     * @param newPriority priority to set this thread to
     * @exception  IllegalArgumentException  If the priority is not in the
     *               range <code>MIN_PRIORITY</code> to
     *               <code>MAX_PRIORITY</code>.
     * @exception  SecurityException  if the current thread cannot modify
     *               this thread.
     * @see        #getPriority
     * @see        #checkAccess()
     * @see        #getThreadGroup()
     * @see        #MAX_PRIORITY
     * @see        #MIN_PRIORITY
     * @see        ThreadGroup#getMaxPriority()
     */
    public final void setPriority(int newPriority) {
        //執行緒組
        ThreadGroup g;
        //檢查安全許可權
        checkAccess();
        //檢查優先順序形參範圍
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            //如果優先順序形參大於執行緒組最大執行緒最大優先順序
            if (newPriority > g.getMaxPriority()) {
                //則使用執行緒組的優先順序資料
                newPriority = g.getMaxPriority();
            }
            //呼叫本地設定執行緒優先順序方法
            setPriority0(priority = newPriority);
        }
    }
複製程式碼

執行緒中斷

有一個stop()例項方法可以強制終止執行緒,不過這個方法因為太過於暴力,已經被標記為過時方法,不建議程式設計師再使用,因為強制終止執行緒會導致資料不一致的問題。

這裡關於執行緒中斷的方法涉及三個:

//例項方法,通知執行緒中斷,設定標誌位
 public void interrupt(){}
 //靜態方法,檢查當前執行緒的中斷狀態,同時會清除當前執行緒的中斷標誌位狀態
 public static boolean interrupted(){}
 //例項方法,檢查當前執行緒是否被中斷,其實是檢查中斷標誌位
 public boolean isInterrupted(){}
複製程式碼

interrupt() 方法解析

/**
     * 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.
     *
     * @throws  SecurityException
     *          if the current thread cannot modify this thread
     *
     * @revised 6.0
     * @spec JSR-51
     */
    public void interrupt() {
        //檢查是否是自身呼叫
        if (this != Thread.currentThread())
            //檢查安全許可權,這可能導致丟擲{@link * SecurityException}。
            checkAccess();
        
        //同步程式碼塊
        synchronized (blockerLock) {
            Interruptible b = blocker;
            //檢查是否是阻塞執行緒呼叫
            if (b != null) {
                //設定執行緒中斷標誌位
                interrupt0(); 
                //此時丟擲異常,將中斷標誌位設定為false,此時我們正常會捕獲該異常,重新設定中斷標誌位
                b.interrupt(this);
                return;
            }
        }
        //如無意外,則正常設定中斷標誌位
        interrupt0();
    }
複製程式碼
  • 執行緒中斷方法不會使執行緒立即退出,而是給執行緒傳送一個通知,告知目標執行緒,有人希望你退出啦~
  • 只能由自身呼叫,否則可能會丟擲 SecurityException
  • 呼叫中斷方法是由目標執行緒自己決定是否中斷,而如果同時呼叫了wait,join,sleep等方法,會使當前執行緒進入阻塞狀態,此時有可能發生InterruptedException異常
  • 被阻塞的執行緒再呼叫中斷方法是不合理的
  • 中斷不活動的執行緒不會產生任何影響

檢查執行緒是否被中斷:

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     
     測試此執行緒是否已被中斷。, 執行緒的<i>中斷*狀態</ i>不受此方法的影響。
     *
     * <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
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
複製程式碼

靜態方法,會清空當前執行緒的中斷標誌位:

   /**
     *測試當前執行緒是否已被中斷。, 此方法清除執行緒的* <i>中斷狀態</ i>。, 換句話說,如果要連續兩次呼叫此方法,則* second呼叫將返回false(除非當前執行緒再次被中斷,在第一次呼叫已清除其中斷的*狀態   之後且在第二次呼叫已檢查之前), 它)
     *
     * <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
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

複製程式碼

總結

記錄自己閱讀Thread類原始碼的一些思考,不過對於其中用到的很多本地方法只能望而卻步,還有一些程式碼沒有看明白,暫且先這樣吧,如果有不足之處,請留言告知我,謝謝!後續會在實踐中對Thread做出更多總結記錄。

最後

由於篇幅較長,暫且先記錄這些吧,後續會不定期更新原創文章,歡迎關注公眾號 「張少林同學」!

從原始碼的角度再學「Thread」

相關文章