併發編程之線程的創建和啓動
一、線程創建
1.1. 實現Runnable
接口
實現Runnable
接口,重寫run
方法,實現Runnable
接口的實現類的實例對象作爲Thread
構造函數的target
:
public class CreateThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通過Runnable創建線程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread createThread = new Thread(new CreateThread(), "子線程");
createThread.start();
System.out.println(Thread.currentThread().getName() + "主線程......");
}
}
我們看一下Runnable
接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
我們可以看到Runnable
被FunctionInterface
接口,說明使用lamdba
的寫法去實現線程。很簡潔。
public class CreateThread {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "主線程......");
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通過Runnable創建線程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "子線程1").start();
}
}
1.2. 繼承Thread
類
繼承Thread
類,重寫run
方法。其實Thread
也是實現了Runnable
接口,裏面有很多native
方法。後面會分析。
public class CreateThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通過Runnable創建線程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread createThread = new CreateThread1();
createThread.setName("子線程");
createThread.start();
System.out.println(Thread.currentThread().getName() + "主線程......");
}
}
我們簡單看見一下Thread
裏面的run
方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
這個裏面的target
其實就是我們傳入的Runnable
,這也是爲啥我們可以實現Runnable
接口的run
方法,這也是就是繼承Thread
(把run
重寫)和實現Runnable
(調用target.run()
方法)的區別。
更值得我們注意的是run
方法是異常的處理和拋出的,這意味的子線程發生異常,主線程是無法捕獲到的(但是具體還是有處理的方法的,日後介紹,挖個坑,日後填)。
1.3. 總結
- 實現
Runnable
接口更好 Runnable
方便配合線程池使用Thread
線程執行和線程創建無法解耦Thread
繼承之後無法繼承其他線程,限制擴展性
最後再說一下 :創建線程我們可以有兩種方法實現Runnable
和繼承Thread
,但是本質來說創建線程只有一種(構造Thread
類),run
方法有兩種傳入Runnable
通過target.run()
調用和重寫run()
方法。
二、線程的啓動
我們從上面的看到,線程的啓動涉及到start()
和run()
兩種方法。
我們先看看start()
方法:
/**
* 1. start方法將導致當前線程開始執行。由JVM調用當前線程的run方法
* 2. 結果是兩個線程同時運行:當前線程(從對start方法的調用返回)和另一個線程(執行其run方法)
* 3. 不允許多次啓動線程, 線程一旦完成執行就不會重新啓動
*/
public synchronized void start() {
/**
* 對於VM創建/設置的主方法線程或“系統”組線程,不調用此方法。
* 將來添加到此方法的任何新功能可能也必須添加到VM中.
*
* threadStatus = 0 意味着線程處於初始狀態
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* 通知組此線程即將啓動,以便可以將其添加到組的線程列表中,並且可以減少組的未啓動計數。*/
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* 不做處理。如果start0拋出了一個Throwable,那麼它將被傳遞到調用堆棧上 */
}
}
}
private native void start0();
從上面代碼的註釋我們可以看到:
start()
方法被synchronized
進行修飾,爲同步方法,這樣避免了多次調用問題;- 使用
threadStatus
(此變量被volatile
修飾)記錄線程狀態;多次調用會拋出異常; - 這方法會重寫的
run
方法被虛擬機調用,是子線程執行的run
方法。
上面已經介紹了start
和run
方法,接着我們寫一個例子看看兩個的區別:
public static void main(String[] args) {
Thread one = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "啓動");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程1");
Thread two = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "啓動");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程2");
one.start();
two.run();
System.out.println("主線程啓動");
}
執行結果也很明顯,調用run
會阻塞主線程,start
是異步的。