Thread類源碼解讀(1)——如何建立和啓動線程

前言

系列文章目錄 html

談到線程同步與通訊,線程自己的概念是繞不開的,而進程和線程的概念已是老生常談的話題了,一些基本的概念本文就再也不討論了,本篇僅僅致力於經過源碼,瞭解線程的構造與啓動,從而更深刻的瞭解線程。java

本文源碼基於jdk1.8 。面試

閱讀完本文,你應當有能力回答如下常見面試題:segmentfault

  1. 建立線程有哪幾種方式?
  2. 如何啓動一個線程?
  3. 線程的run方法和start方法有什麼區別?

Runnale接口

咱們看Thread類的定義知道,它實現了Runable接口api

public class Thread implements Runnable {
    ...
}

Runnable接口的定義以下:多線程

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

它只有一個抽象方法run。同時,該接口還被@FunctionalInterface註解標註,說明它是一個函數式接口(@FunctionalInterface是java 1.8版本以後引入的)。這意味着咱們可使用Lambda表達式來建立Runnable接口的實例,這個咱們到後面再舉例。併發

線程建立

在java中,建立一個線程,有且僅有一種方式: oracle

建立一個Thread類實例,並調用它的start方法。 ide

這寫在了java語言規範中(參見The Java Language Specification, Java SE 8 Edition, P659,chapter17):函數

Threads are represented by the Thread class. The only way for a user to create a thread is to create an object of this class; each thread is associated with such an object. A thread will start when the start() method is invoked on the corresponding Thread object.

構造函數

要建立一個Thread類的實例天然要經過構造函數,Thread的public構造函數有8個之多,可是他們本質上都調用了同一個init函數:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
    init(null, null, name, 0);
}
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target) {
    init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
    init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
    init(group, target, name, stackSize);
}

可見,這八個public類型的構造函數只不過是給init的方法的四個參數分別賦不一樣的值, 這四個參數分別是:

  • ThreadGroup g(線程組)
  • Runnable target (Runnable 對象)
  • String name (線程的名字)
  • long stackSize (爲線程分配的棧的大小,若爲0則表示忽略這個參數)

而init方法又調用了另外一個init方法,設置了AccessController,以及inheritThreadLocals參數:

/**
 * Initializes a Thread with the current AccessControlContext.
 * @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean)
 */
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
}

//上面那個init方法最終調用了下面這個方法:

/**
 * Initializes a Thread.
 *
 * @param g the Thread group
 * @param target the object whose run() method gets called
 * @param name the name of the new Thread
 * @param stackSize the desired stack size for the new thread, or
 *        zero to indicate that this parameter is to be ignored.
 * @param acc the AccessControlContext to inherit, or
 *            AccessController.getContext() if null
 * @param inheritThreadLocals if {@code true}, inherit initial values for
 *            inheritable thread-locals from the constructing thread
 */
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    ...
}

init方法中有一些關於線程組和訪問控制上下文的設置,這裏咱們暫時就不深刻討論了。

因此綜上來看,咱們最經常使用的也就兩個參數:

  • Runnable target (Runnable 對象)
  • String name (線程的名字)

而對於線程的名字,其默認值爲"Thread-" + nextThreadNum(), nextThreadNum方法又是什麼呢:

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

可見,它就是一個簡單的遞增計數器,因此若是建立線程時沒有指定線程名,那線程名就會是:
Thread-0, Thread-1, Thread-2, Thread-3, ...

至此,咱們看到,雖然Thread類的構造函數有這麼多,但對咱們來講真正重要的參數只有一個:

Runnable target (Runnable 對象)

因此建立一個線程實例最重要的是要傳入一個Runnable類型對象。

既然是Runnable類型,那麼這個target必然是實現了Runnable接口的,也就是說該對象必定覆寫了run方法。

咱們知道,Thread類自己也實現了Runnable接口,因此它必然也覆寫了run方法,咱們先來看看它的run方法:

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

能夠看到,這個run方法僅僅是調用了target對象的run方法,若是咱們在線程構造時沒有傳入target(例如調用了無參構造函數),那麼這個run方法就什麼也不會作。

啓動線程

線程對象建立完了以後,接下來就是啓動一個線程,在java中,啓動一個線程必須調用線程的start方法:

/**
 * 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.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
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()

這個方法本質是調用了native的start0()方法,可是它的註釋部分說明一些很重要的信息:

這個方法使得線程開始執行,並由JVM來執行這個線程的run方法,結果就是有兩個線程在併發執行,一個是當前線程,也就是調用了Thread#start方法的線程,另外一個線程就是當前thread對象表明的線程,它執行了run方法。

也就是說,這個Thread類實例表明的線程最終會執行它的run方法,而上面的分析中咱們知道,它的run作的事就是調用Runnable對象的run方法,若是Runnable對象爲null, 就啥也不作:

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

有的同窗就要問了,繞了一大圈,忙了大半天,最後不就是爲了執行target對象的run方法嗎?爲何咱們不直接調用target的run方法?這一層層的調用到底是爲了啥? 答案是:

爲了使用多線程 !

咱們知道,Thread類從定義上看就是個普通的java類,是什麼魔法讓它從一個普通的java類晉升爲一個能夠表明線程的類呢?是native方法!

若是咱們直接調用target對象的run方法,或者Thread類的run方法,那就是一個普通調用,由於run方法就是普普統統的類方法,與咱們平時調用的其餘類方法沒有什麼不一樣,這並不會產生多線程。

可是,若是咱們調用了start方法,因爲它內部使用了native方法來啓動線程,它將致使一個新的線程被建立出來, 而咱們的Thread實例, 就表明了這個新建立出來的線程, 而且由這個新建立出來的線程來執行Thread實例的run方法。

實戰

說了這麼多理論的東西,下面讓咱們經過一個實戰來加深理解。java官方文檔給咱們提供了兩種建立線程的方法.

方法1:繼承Thread類,覆寫run方法

首先咱們自定義一個繼承自Thread的類,並覆寫run方法:

public class CustomizedThread extends Thread {
    public void run() {
        System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我是定義在CustomizedThread類中的run方法。");
    }
}

而後咱們建立類的實例,並調用start方法啓動這個線程:

public class CustomizedThreadTest {
    public static void main(String[] args) {
        System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法裏");
        CustomizedThread myThread = new CustomizedThread();
        myThread.start();
    }
}

執行結果:

[main線程]: 我在main方法裏
[Thread-0線程]: 我是定義在CustomizedThread類中的run方法。

可見,這裏有兩個線程,一個是main線程,它執行了main方法,一個是Thread-0線程,它是咱們自定義的線程,它執行了run方法。

若是咱們不經過start方法來運行線程會有什麼不一樣呢:

public class CustomizedThreadTest {
    public static void main(String[] args) {
        System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法裏");
        CustomizedThread myThread = new CustomizedThread();
        //myThread.start();
        myThread.run();
    }
}

這裏咱們直接調用自定義線程的run方法,看看結果有什麼不一樣:

[main線程]: 我在main方法裏
[main線程]: 我是定義在CustomizedThread類中的run方法。

可見,此次只有一個main線程,由main線程執行了咱們自定義線程類的run方法,並無新的線程產生。 其實這個時候,CustomizedThread的run方法就是一個普普統統的類的普普統統的方法,與咱們平時定義的方法並無什麼特別之處。

有的同窗要問了,上面不是說建立一個線程最重要的是傳入一個Runnable對象嗎? 我沒有看到Runnable對象啊? 別急,咱們來分析一下:

首先,咱們的CustomizedThread繼承自Thread類,則咱們會調用父類的無參構造函數:

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

這個構造函數中,target對象爲null;

而後,咱們使用了myThread.start(),由於咱們在子類中沒有定義start方法,因此,這個方法來自父類,而Thread類的start方法的做用咱們已經講過,它將新建一個線程,並調用它的run方法,這個新建的線程的抽象表明就是咱們的CustomizedThread,因此它的(CustomizedThread的)run方法將會被調用。

那麼,若是咱們的子類沒有覆寫run方法呢?,那天然是繼承Thread類本身的run方法了:

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

而Thread類的run方法調用的又是target對象的run方法,而target對象如今爲null, 因此這個方法啥也不作。

因此到這裏咱們就很清晰了,建立一個線程最重要的是定義一個run方法,這個run方法要麼經過繼承Thread類的子類覆寫,要麼經過直接構造Thread類時傳入一個Runnable的target對象。不管它由子類覆寫提供仍是由target對象提供,start方法最終都會新建一個線程來執行這個run方法。

方法2:經過Runnable接口建立線程類

咱們先來看官方的例子:

class PrimeRun implements Runnable {
     long minPrime;
     PrimeRun(long minPrime) {
         this.minPrime = minPrime;
     }

     public void run() {
         // compute primes larger than minPrime
          . . .
     }
}
//The following code would then create a thread and start it running:

 PrimeRun p = new PrimeRun(143);
 new Thread(p).start();

這個例子中首先定義了一個PrimeRun類實現了Runnable接口,接着實例化出一個對象p,並將這個對象做爲參數傳遞給Thread類的構造方法。

這種方法看上去好像複雜了好多,但其實就是經過新建Thread類的對象來建立線程。它本質上就是傳遞一個Runnable對象給Thread的構造函數,因此咱們徹底能夠用匿名類,又由於Runnable是一個函數接口,因此上面的代碼徹底能夠被簡寫,咱們來看一個例子:

public class CustomizedThreadTest {
    public static void main(String[] args) {
        System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法裏");
        Thread myThread = new Thread(() -> System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我是傳遞給Thread類的Runnable對象的run方法"));
        myThread.start();
    }
}

代碼輸出:

[main線程]: 我在main方法裏
[Thread-0線程]: 我是傳遞給Thread類的Runnable對象的run方法

這裏,myThread是咱們new出來的Thread類的實例,咱們調用了Thread類的構造函數:

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

傳入了一個Runnable對象,這個Runnable對象由lambda表達式表示。咱們最後調用了 myThread.start()來啓動這個線程,經過上一節的分析咱們知道,start方法會調用run方法,而thread類的run方法最終會調用target對象的run方法,而target對象的run方法就是咱們傳進來的lambda表達式。上面這個例子其實等效於下面這種寫法:

public class CustomizedThreadTest {
    public static void main(String[] args) {
        System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法裏");
        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我是傳遞給Thread類的Runnable對象的run方法");
            }
        });
        myThread.start();
    }
}

可見函數式接口和lambda表達式使咱們的書寫變得簡潔多了。

總結

在java中,建立一個線程,有且僅有一種方式:

建立一個Thread類實例,並調用它的start方法。

建立一個Thread類的實例最重要的是定義一個run方法,這個run方法說明了這個線程具體要作什麼事情。有兩種方式定義一個run方法:

  1. 繼承Thread類,覆寫run方法
  2. 實現Runnale接口,將它做爲target參數傳遞給Thread類構造函數

啓動一個線程必定要調用該線程的start方法,不然,並不會建立出新的線程來。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索