線程是實現多線程的基礎,本篇主要講解關於線程的兩個問題。
1 爲什麼說本質上只有一種實現多線程的方式?
2 實現Runnable的方式和繼承Thread的方式哪種好?好在哪裏?
爲什麼說本質上只有一種實現多線程的方式?
- 在回答這個問題之前,我們首先看看常用的實現多線程的方式。
1 實現Runnable接口的方式
class RunnableImpl : Runnable {
override fun run() {
println("this is test for runnable")
}
}
fun main() {
Thread(RunnableImpl()).start()
}
- 通過實現Runnable接口的方式,並且實現其中的
run
方法,並將實現run方法的實例對象作爲Thread的構造方法參數,然後調用新創建的Thread的start方法,就可以實現多線程了。
2 繼承Thread類的方式
class ThreadExtend : Thread() {
override fun run() {
println("this is test for thread")
}
}
fun main() {
ThreadExtend().start()
}
- 通過繼承Thread的方式,並且重寫其中的run方法,然後用此實例對象調用start方法即可實現多線程。
3 通過Executors線程池模型實現多線程
fun main() {
Executors.newSingleThreadExecutor().execute {
println("thread pool")
}
}
- 通過
Executors
靜態工廠方法模式,可以創建多種ExecutorService
的ThreadPoolExecutor
實例對象。其中ThreadPoolExecutor中有一個參數爲ThreadFactory
,它的默認實現是DefaultThreadFactory
,下面我們看一下它的實現:
public interface ThreadFactory {
Thread newThread(Runnable r);
}
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
- 線程池模型中的線程是怎麼來的呢?主要就是看
newThread
方法的實現了,我們可以看到首先通過構造方法傳入了方法名字,然後設置是否爲守護線程,設置線程的優先級,最終將此線程返回了。所以通過線程池創建線程的方式還是屬於通過前面new Thread()
的方式實現的。
4 通過Callable實現多線程
class CallableImpl : Callable<String> {
override fun call(): String {
Thread.sleep(3000)
return "callable impl"
}
}
fun main() {
println("before future get")
Executors.newSingleThreadExecutor().submit(CallableImpl()).let {
println(it.get())
}
println("after future get")
}
- 通過Callable的方式和Runnable的方式的區別是前者是有返回值的,並且調用future.get()方法會阻塞當前線程直到獲取到返回值。但是實際上調用了
submit(callable)
之後,內部還是將Callable封裝爲了Runnable,這種實現多線程的方式還是離不開最開始說的兩種方式。
實現多線程只有一種方式
- 其實通過上面的例子就會發現,不管實現多線程的方式千變萬化,肯定離不開上面最開始的兩種方式,下面我們對最開始的兩種方式做一個分析,來回答爲什麼說本質上只有一種實現多線程的方式?
- 調用Thread的start方法,會執行run方法,我們查看Thread中run方法的源碼:
class Thread { public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private Runnable target; @Override public void run() { if (target != null) { target.run(); } } }
- 如果通過實現Runnable的方式的話,執行run方法的時候,內部的target不爲null,執行的就是我們自定義Runnable中實現的run方法的內容。
- 如果通過繼承Thread的方式的話,就會重寫這裏的run方法,就執行的是自定義的Thread的run方法。
- 所以實際上實現多線程唯一的方式就是通過構造一個Thread類,這也是本質上實現多線程的唯一方式。
- 其實上面不管是上面的Runnable也好,Callable也好,重寫Thread的run方法也好,他們並沒有實現多線程的功能,他們只是提供了多線程需要執行的內容,按照此思路,實現多線程的方式就會越來越多,但是本質上還是上面說的唯一的一種方式。
Runnable和Thread哪種方式好?好在哪裏?
下面我們對這兩種方式做一下對比,就知道Runnable方式爲什麼比Thread方式好。
- 1 從代碼架構的角度來,實現Runnable的方式,內部只定義了需要執行的內容,這樣就將線程的執行和執行的內容分開了,達到了解耦的目的。
- 2 從性能方方面考慮,因爲線程的創建和銷燬是有開銷的,如果創建和銷燬線程所引起的性能消耗遠遠大於執行run方法所帶來的性能消耗,這樣的話頻繁的創建線程和銷燬線程就得不償失了,我們可以將任務用Runnable的方式傳入線程池中,交給線程池來維護線程。
- 3 從擴展性來講,我們直到Java不支持多繼承,但是支持多實現,如果我們自定義了Thread類,已經繼承了Thread類,想要再繼承其它類就不可以了,限制了它在未來的擴展性。
- 綜上所述Runnable方式更加優於Thread方式實現多線程。