多線程編程一-創建線程的三種方式

目錄

1 繼承Thread類

2 實現Runnable接口

3 線程啓動原理

3.1 start 方法

3.2 run方法

4 實現Callable接口

4.1 Callable初步使用

4.2 Callable方法原理

4.2.1 FutureTask run方法實現

4.2.2 get方法

4.2.3 FutureTask不可重複使用


java支持三種線程創建方式:繼承Thread類,實現Runnable接口,實現Callable接口

1 繼承Thread類

@Test
public void testThread() throws InterruptedException {
    new MyThread().start();
    TimeUnit.SECONDS.sleep(1);
}

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("thread start");
    }
}

輸出thread start

2 實現Runnable接口

@Test
public void testRunnable() throws InterruptedException {
    new Thread(new MyRunnable()).start();
    TimeUnit.SECONDS.sleep(1);
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("runnable start");
    }
}

輸出runnable start

3 線程啓動原理

通過上述代碼我們知道線程啓動是通過start方法啓動的,那麼咱們看一下start方法

3.1 start 方法

public synchronized void start() {
    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) {
        }
    }
}

start最終會執行start0方法,這是一個本地方法,會把線程設置爲就緒狀態,等待CPU調度。

3.2 run方法

我們都知道線程啓動後最終會調用run方法,我們看一下run方法的實現
private Runnable target;
public void run() {
    if (target != null) {
        target.run();
    }
}

target在通過實現Runnable創建線程的時候注入給Thread類,所以我們可以知道繼承Thread類和實現Runnable接口本質上沒有太大區別

4 實現Callable接口

4.1 Callable初步使用

@Test
public void testCallable() throws InterruptedException, ExecutionException {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
    new Thread(futureTask).start();
    System.out.println(DateUtil.datetimeToString(new Date()) + ":" + futureTask.get());
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(DateUtil.datetimeToString(new Date()) + ":" + "callable start");
        TimeUnit.SECONDS.sleep(2);
        return 1;
    }
}

輸出
2020-06-26 23:05:26:callable start
2020-06-26 23:05:28:1
這裏可以看到,兩次輸出是間隔兩秒的,所以get方法是阻塞方法

4.2 Callable方法原理

4.2.1 FutureTask run方法實現

我們知道Thread的構造器是Runnable接口引用的對象,FutureTask實現了又是實現Runnable接口,所以線程啓動的時候調的也是FutureTask的run方法

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        // 如果state狀態不是線程創建狀態或者當前佔用線程設置失敗直接返回
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                // 獲取線程call方法結果
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                // 如果call方法已經運行,設置運行後的結果
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

接下來咱們看看set設置執行結果的方法

protected void set(V v) {
    // 把當前線程生命週期從新建改爲運行完成
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 把call執行結果賦值給outcame變量
        outcome = v;
        // 把state設置爲NORMAL狀態
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

分析源碼我們可以知道FutureTask方法實際上就是運行Callable的call方法,把運行結果賦值給成員變量outcome,並且把state值最終設置爲NORMAL

4.2.2 get方法

get方法比較簡單,如果call方法已經運行完成,直接返回outcome值;否則等待運行完成

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        // 如果還沒運行完成則等待運行完成
        s = awaitDone(false, 0L);
    // 返回結果
    return report(s);
}

// 如果是NORMAL狀態直接返回,否者拋異常
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

4.2.3 FutureTask不可重複使用

下面咱們把測試代碼稍微改一下,MyCallable類隨機返回一個整數,然後用兩個線程啓動一個FutureTask類查看返回數據
測試代碼如下

@Test
public void testCallable() throws InterruptedException, ExecutionException {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
    new Thread(futureTask).start();
    Integer result = futureTask.get();
    System.out.println(DateUtil.datetimeToString(new Date()) + ":" + result);
    new Thread(futureTask).start();
    result = futureTask.get();
    System.out.println(DateUtil.datetimeToString(new Date()) + ":" + result);
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(DateUtil.datetimeToString(new Date()) + ":" + "callable start");
        TimeUnit.SECONDS.sleep(2);
        return new Random().nextInt(1000);
    }
}

輸出
2020-06-26 23:24:30:callable start
2020-06-26 23:24:32:711
2020-06-26 23:24:32:711
只輸出了一次callable start,說明call方法只運行了一次。get直接輸出了711,並且沒有阻塞,可以說明第二次get的結果讀取的是第一個執行線程運行的結果
原因也很簡單,我們再看看run方法代碼,其中任意一個線程執行結束後會把state設置爲NORMAL,下一個線程運行的時候判斷不是NEW狀態直接return掉了。
所以我們如果想用兩個線程都要執行Callable方法一定不能偷懶,要用兩個FutureTask實例來運行線程,正確姿勢如下

@Test
public void testCallable() throws InterruptedException, ExecutionException {
    MyCallable myCallable = new MyCallable();
    FutureTask<Integer> task1 = new FutureTask<>(myCallable);
    FutureTask<Integer> task2 = new FutureTask<>(myCallable);
    // 這裏同時啓動兩個線程,提高執行效率
    new Thread(task1).start();
    new Thread(task2).start();
    Integer result = task1.get();
    System.out.println(DateUtil.datetimeToString(new Date()) + ":" + result);
    result = task2.get();
    System.out.println(DateUtil.datetimeToString(new Date()) + ":" + result);
}

輸出
2020-06-26 23:33:20:callable start
2020-06-26 23:33:20:callable start
2020-06-26 23:33:22:889
2020-06-26 23:33:22:748

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章