一、背景
本文給出兩個簡單卻很有意思的線程相關的題目
題目1:
Java 中有幾種建立線程的方式?
如果面試中遇到這個問題,估計很多人會非常開心,然而網上的諸多答案真的對嗎?
題目2:
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run")) {
@Override
public void run() {
System.out.println("Thread run");
}
};
thread.start();
}
請問運作後輸出的結果是啥?
拿到這個問題有些同學可能會懵掉幾秒鐘,什麼鬼...
二、分析
2.1 有幾種建立形成的方式
不知道大家想過沒有,本質上 JDK 8 中提供了幾種建立線程的方式?
可能很多人會講可以先建立 Runnable 當做參數傳給 Thread ,可以寫匿名内部類,可以編寫 Thread 的子類,可以通過線程池等等。
其實線程池的 Worker 内部還是通過 Thread 執行的,而Worker 中的線程是通過 ThreadFactory 建立,ThreadFactory 最終還是通過構造 Thread 或者 Thread 子類的方式建立線程的。
以
org.apache.tomcat.util.threads.TaskThreadFactory
為例:
/**
* Simple task thread factory to use to create threads for an executor
* implementation.
*/
public class TaskThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final boolean daemon;
private final int threadPriority;
public TaskThreadFactory(String namePrefix, boolean daemon, int priority) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.namePrefix = namePrefix;
this.daemon = daemon;
this.threadPriority = priority;
}
@Override
public Thread newThread(Runnable r) {
TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(daemon);
t.setPriority(threadPriority);
// Set the context class loader of newly created threads to be the class
// loader that loaded this factory. This avoids retaining references to
// web application class loaders and similar.
if (Constants.IS_SECURITY_ENABLED) {
PrivilegedAction<Void> pa = new PrivilegedSetTccl(
t, getClass().getClassLoader());
AccessController.doPrivileged(pa);
} else {
t.setContextClassLoader(getClass().getClassLoader());
}
return t;
}
}
TaskThread
源碼:
public class TaskThread extends Thread {
//...
}
其實從本質上講隻有一種建立對象的方式,就是通過建立 Thread 或者子類的方式。
接下來讓我們看下 Thread 類的注釋:
/**
* There are two ways to create a new thread of execution.
* One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre>
* class PrimeThread extends Thread {
* long minPrime;
* PrimeThread(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* . . .
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeThread p = new PrimeThread(143);
* p.start();
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
* class PrimeRun implements Runnable {
* long minPrime;
* PrimeRun(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* . . .
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeRun p = new PrimeRun(143);
* new Thread(p).start();
* </pre></blockquote>
* <p>
*/
public class Thread implements Runnable {
// 省略其他
}
通過注釋我們可以看出有兩種建立執行線程的方式:
- 繼承 Thread 并且重寫 run 方法。
- 實作 Runnable 接口實作 run 方法,并作為參數來建立 Thread。
如果是從這個層面上講,有兩種建立 Thread 的方式,其他方式都是這兩種方式的變種。
2.2 運作結果是啥?
2.2.1 回顧
可能很多同學看到這裡會有些懵。
如果下面這種寫法(寫法1):
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run"));
thread.start();
}
答案是列印: "Runnable run"
如果這麼寫(寫法2):
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread run");
}
};
thread.start();
}
答案是列印“Thread run”
一起寫,這個操作有點風騷...
2.2.2 莫慌
我們可以确定的是 thread.start 調用的是 run 方法,既然這裡重寫了 run 方法,肯定調用的是咱們重寫的 run 方法。
是以答案是 "Thread run“。
為了更好地搞清楚這個問題,咱麼看下源碼:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
target 部分:
/* What will be run. */
private Runnable target;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ....
this.target = target;
}
我們再看下預設的 run 方法:
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
注釋說的很清楚,通過構造方法傳入 Runnable ,則調用 Runnable的 run 方法,否則啥都不幹。
是以這就是為什麼寫法1 的結果是:"Runnable run"。
如果咱們重寫了 run 方法,預設 target 的就失效了,是以結果就是"Thread run“。
如果我想先執行 Runnbale 的 run 方法再執行咱們的列印"Thread run“咋辦?
既然上面了解到父類的預設行為是執行構造函數中 Runnbale 對象的 run 方法,咱麼先調用 super.run 不就可以了嗎:
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run")) {
@Override
public void run() {
super.run();
System.out.println("Thread run");
}
};
thread.start();
}
輸出結果:
Runnable run
Thread run
其實這個題目重點考察大家有沒有認真看過源碼,有沒有真正了解多态的含義,是否都線程基礎有一定的了解。
三、總結
這個問題本質上很簡單,實際上很多人第一反應會懵掉,是因為很少主動去看 Thread 源碼。
學習和工作的時候更多地是學會用,而不是多看源碼,了解原理。
通過這個簡單的問題,希望大家學習和工作之餘可以養成檢視源碼的習慣,多動手練習,多思考幾個為什麼。
希望大家讀書時,尤其是看部落格文章時,不要想當然,多思考下問題的本質。
如果你覺得本文對你有幫助,歡迎點贊評論,你的支援和鼓勵是我創作的最大動力。
