摘要:無論它由子類覆寫提供還是由對象提供,方法最終都會新建一個線程來執(zhí)行這個方法。這種方法看上去好像復(fù)雜了好多,但其實就是通過新建類的對象來創(chuàng)建線程??偨Y(jié)在中,創(chuàng)建一個線程,有且僅有一種方式創(chuàng)建一個類實例,并調(diào)用它的方法。
前言
系列文章目錄
談到線程同步與通信,線程本身的概念是繞不開的,而進(jìn)程和線程的概念已經(jīng)是老生常談的話題了,一些基本的概念本文就不再討論了,本篇僅僅致力于通過源碼,了解線程的構(gòu)造與啟動,從而更深入的了解線程。
本文源碼基于jdk1.8 。
閱讀完本文,你應(yīng)當(dāng)有能力回答以下常見面試題:
創(chuàng)建線程有哪幾種方式?
如何啟動一個線程?
線程的run方法和start方法有什么區(qū)別?
Runnale接口我們看Thread類的定義知道,它實現(xiàn)了Runable接口
public class Thread implements Runnable { ... }
而Runnable接口的定義如下:
@FunctionalInterface public interface Runnable { public abstract void run(); }
它只有一個抽象方法run。同時,該接口還被@FunctionalInterface注解標(biāo)注,說明它是一個函數(shù)式接口(@FunctionalInterface是java 1.8版本之后引入的)。這意味著我們可以使用Lambda表達(dá)式來創(chuàng)建Runnable接口的實例,這個我們到后面再舉例。
線程創(chuàng)建在java中,創(chuàng)建一個線程,有且僅有一種方式:
創(chuàng)建一個Thread類實例,并調(diào)用它的start方法。
這寫在了java語言規(guī)范中(參見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.構(gòu)造函數(shù)
要創(chuàng)建一個Thread類的實例自然要通過構(gòu)造函數(shù),Thread的public構(gòu)造函數(shù)有8個之多,但是他們本質(zhì)上都調(diào)用了同一個init函數(shù):
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類型的構(gòu)造函數(shù)只不過是給init的方法的四個參數(shù)分別賦不同的值, 這四個參數(shù)分別是:
ThreadGroup g(線程組)
Runnable target (Runnable 對象)
String name (線程的名字)
long stackSize (為線程分配的棧的大小,若為0則表示忽略這個參數(shù))
而init方法又調(diào)用了另一個init方法,設(shè)置了AccessController,以及inheritThreadLocals參數(shù):
/** * 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方法最終調(diào)用了下面這個方法: /** * 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方法中有一些關(guān)于線程組和訪問控制上下文的設(shè)置,這里我們暫時就不深入討論了。
所以綜上來看,我們最常用的也就兩個參數(shù):
Runnable target (Runnable 對象)
String name (線程的名字)
而對于線程的名字,其默認(rèn)值為"Thread-" + nextThreadNum(), nextThreadNum方法又是什么呢:
/* For autonumbering anonymous threads. */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }
可見,它就是一個簡單的遞增計數(shù)器,所以如果創(chuàng)建線程時沒有指定線程名,那線程名就會是:
Thread-0, Thread-1, Thread-2, Thread-3, ...
至此,我們看到,雖然Thread類的構(gòu)造函數(shù)有這么多,但對我們來說真正重要的參數(shù)只有一個:
Runnable target (Runnable 對象)
所以創(chuàng)建一個線程實例最重要的是要傳入一個Runnable類型對象。
既然是Runnable類型,那么這個target必然是實現(xiàn)了Runnable接口的,也就是說該對象一定覆寫了run方法。
我們知道,Thread類本身也實現(xiàn)了Runnable接口,所以它必然也覆寫了run方法,我們先來看看它的run方法:
@Override public void run() { if (target != null) { target.run(); } }
可以看到,這個run方法僅僅是調(diào)用了target對象的run方法,如果我們在線程構(gòu)造時沒有傳入target(例如調(diào)用了無參構(gòu)造函數(shù)),那么這個run方法就什么也不會做。
啟動線程線程對象創(chuàng)建完了之后,接下來就是啟動一個線程,在java中,啟動一個線程必須調(diào)用線程的start方法:
/** * Causes this thread to begin execution; the Java Virtual Machine * calls therun
method of this thread. ** The result is that two threads are running concurrently: the * current thread (which returns from the call to the *
start
method) and the other thread (which executes its *run
method). ** 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()
這個方法本質(zhì)是調(diào)用了native的start0()方法,但是它的注釋部分說明一些很重要的信息:
這個方法使得線程開始執(zhí)行,并由JVM來執(zhí)行這個線程的run方法,結(jié)果就是有兩個線程在并發(fā)執(zhí)行,一個是當(dāng)前線程,也就是調(diào)用了Thread#start方法的線程,另一個線程就是當(dāng)前thread對象代表的線程,它執(zhí)行了run方法。
也就是說,這個Thread類實例代表的線程最終會執(zhí)行它的run方法,而上面的分析中我們知道,它的run做的事就是調(diào)用Runnable對象的run方法,如果Runnable對象為null, 就啥也不做:
@Override public void run() { if (target != null) { target.run(); } }
有的同學(xué)就要問了,繞了一大圈,忙了大半天,最后不就是為了執(zhí)行target對象的run方法嗎?為什么我們不直接調(diào)用target的run方法?這一層層的調(diào)用究竟是為了啥? 答案是:
為了使用多線程 !
我們知道,Thread類從定義上看就是個普通的java類,是什么魔法讓它從一個普通的java類晉升為一個可以代表線程的類呢?是native方法!
如果我們直接調(diào)用target對象的run方法,或者Thread類的run方法,那就是一個普通調(diào)用,因為run方法就是普普通通的類方法,與我們平時調(diào)用的其他類方法沒有什么不同,這并不會產(chǎn)生多線程。
但是,如果我們調(diào)用了start方法,由于它內(nèi)部使用了native方法來啟動線程,它將導(dǎo)致一個新的線程被創(chuàng)建出來, 而我們的Thread實例, 就代表了這個新創(chuàng)建出來的線程, 并且由這個新創(chuàng)建出來的線程來執(zhí)行Thread實例的run方法。
實戰(zhàn)說了這么多理論的東西,下面讓我們通過一個實戰(zhàn)來加深理解。java官方文檔給我們提供了兩種創(chuàng)建線程的方法.
方法1:繼承Thread類,覆寫run方法首先我們自定義一個繼承自Thread的類,并覆寫run方法:
public class CustomizedThread extends Thread { public void run() { System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我是定義在CustomizedThread類中的run方法。"); } }
然后我們創(chuàng)建類的實例,并調(diào)用start方法啟動這個線程:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法里"); CustomizedThread myThread = new CustomizedThread(); myThread.start(); } }
執(zhí)行結(jié)果:
[main線程]: 我在main方法里 [Thread-0線程]: 我是定義在CustomizedThread類中的run方法。
可見,這里有兩個線程,一個是main線程,它執(zhí)行了main方法,一個是Thread-0線程,它是我們自定義的線程,它執(zhí)行了run方法。
如果我們不通過start方法來運(yùn)行線程會有什么不同呢:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "線程]: " + "我在main方法里"); CustomizedThread myThread = new CustomizedThread(); //myThread.start(); myThread.run(); } }
這里我們直接調(diào)用自定義線程的run方法,看看結(jié)果有什么不同:
[main線程]: 我在main方法里 [main線程]: 我是定義在CustomizedThread類中的run方法。
可見,這次只有一個main線程,由main線程執(zhí)行了我們自定義線程類的run方法,并沒有新的線程產(chǎn)生。 其實這個時候,CustomizedThread的run方法就是一個普普通通的類的普普通通的方法,與我們平時定義的方法并沒有什么特別之處。
有的同學(xué)要問了,上面不是說創(chuàng)建一個線程最重要的是傳入一個Runnable對象嗎? 我沒有看到Runnable對象?。?別急,我們來分析一下:
首先,我們的CustomizedThread繼承自Thread類,則我們會調(diào)用父類的無參構(gòu)造函數(shù):
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }
這個構(gòu)造函數(shù)中,target對象為null;
然后,我們使用了myThread.start(),因為我們在子類中沒有定義start方法,所以,這個方法來自父類,而Thread類的start方法的作用我們已經(jīng)講過,它將新建一個線程,并調(diào)用它的run方法,這個新建的線程的抽象代表就是我們的CustomizedThread,所以它的(CustomizedThread的)run方法將會被調(diào)用。
那么,如果我們的子類沒有覆寫run方法呢?,那自然是繼承Thread類自己的run方法了:
@Override public void run() { if (target != null) { target.run(); } }
而Thread類的run方法調(diào)用的又是target對象的run方法,而target對象現(xiàn)在為null, 所以這個方法啥也不做。
所以到這里我們就很清晰了,創(chuàng)建一個線程最重要的是定義一個run方法,這個run方法要么通過繼承Thread類的子類覆寫,要么通過直接構(gòu)造Thread類時傳入一個Runnable的target對象。無論它由子類覆寫提供還是由target對象提供,start方法最終都會新建一個線程來執(zhí)行這個run方法。
方法2:通過Runnable接口創(chuàng)建線程類我們先來看官方的例子:
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類實現(xiàn)了Runnable接口,接著實例化出一個對象p,并將這個對象作為參數(shù)傳遞給Thread類的構(gòu)造方法。
這種方法看上去好像復(fù)雜了好多,但其實就是通過新建Thread類的對象來創(chuàng)建線程。它本質(zhì)上就是傳遞一個Runnable對象給Thread的構(gòu)造函數(shù),所以我們完全可以用匿名類,又因為Runnable是一個函數(shù)接口,所以上面的代碼完全可以被簡寫,我們來看一個例子:
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類的實例,我們調(diào)用了Thread類的構(gòu)造函數(shù):
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
傳入了一個Runnable對象,這個Runnable對象由lambda表達(dá)式表示。我們最后調(diào)用了 myThread.start()來啟動這個線程,通過上一節(jié)的分析我們知道,start方法會調(diào)用run方法,而thread類的run方法最終會調(diào)用target對象的run方法,而target對象的run方法就是我們傳進(jìn)來的lambda表達(dá)式。上面這個例子其實等效于下面這種寫法:
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(); } }
可見函數(shù)式接口和lambda表達(dá)式使我們的書寫變得簡潔多了。
總結(jié)在java中,創(chuàng)建一個線程,有且僅有一種方式:
創(chuàng)建一個Thread類實例,并調(diào)用它的start方法。
創(chuàng)建一個Thread類的實例最重要的是定義一個run方法,這個run方法說明了這個線程具體要做什么事情。有兩種方式定義一個run方法:
繼承Thread類,覆寫run方法
實現(xiàn)Runnale接口,將它作為target參數(shù)傳遞給Thread類構(gòu)造函數(shù)
啟動一個線程一定要調(diào)用該線程的start方法,否則,并不會創(chuàng)建出新的線程來。
(完)
查看更多系列文章:系列文章目錄
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/76743.html
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續(xù)更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因為寫作的時候發(fā)現(xiàn),為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
摘要:如果線程還存活,線程就無限期等待,并讓出監(jiān)視器鎖,進(jìn)入狀態(tài)。當(dāng)線程從狀態(tài)被喚醒后通過,或者是假喚醒將繼續(xù)競爭監(jiān)視器鎖,當(dāng)成功獲得監(jiān)視器鎖后,他將從調(diào)用的地方恢復(fù),繼續(xù)運(yùn)行。 前言 系列文章目錄 上一篇我們討論了線程的創(chuàng)建,本篇我們來聊一聊線程的狀態(tài)轉(zhuǎn)換以及常用的幾個比較重要的方法。 本篇依然是通過源碼分析來了解這些知識。 本文源碼基于jdk1.8 。 閱讀完本文,你應(yīng)當(dāng)有能力回答以...
摘要:線程池為線程生命周期的開銷和資源不足問題提供了解決方案。狀態(tài)說明線程池處于狀態(tài),不接收新任務(wù),不處理已提交的任務(wù),并且會中斷正在處理的任務(wù)。線程池中允許的最大線程數(shù)。線程池的飽和策略。 線程池為線程生命周期的開銷和資源不足問題提供了解決方 案。通過對多個任務(wù)重用線程,線程創(chuàng)建的開銷被分?jǐn)偟搅硕鄠€任務(wù)上。 線程實現(xiàn)方式 Thread、Runnable、Callable //實現(xiàn)Runna...
閱讀 1831·2021-09-27 14:02
閱讀 3688·2021-09-27 13:36
閱讀 1103·2019-08-30 12:46
閱讀 1903·2019-08-30 10:51
閱讀 3655·2019-08-29 17:02
閱讀 1030·2019-08-29 16:38
閱讀 1902·2019-08-29 16:37
閱讀 3160·2019-08-26 10:32