1. 程式人生 > 實用技巧 >執行緒的三種實現方式

執行緒的三種實現方式

執行緒的三種實現方式

  • 繼承Thread類(Thread類也實現了Runnable介面)
  • 實現Runnable介面
  • 實現Callable介面

繼承Thread類

程式碼實現

// 1. 繼承Thread類
public class MyThread extends Thread {
    // 2. 重寫run方法
    @Override
    public void run() {
        // 執行緒體
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " *** " + i);
        }
    }

    public static void main(String[] args) {
        // 主執行緒體

        // 3. 例項化執行緒物件
        MyThread myThread = new MyThread();
        // 4. 啟動執行緒
        myThread.start();

        // 主執行緒中其他操作
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " --- " + i);
        }
    }
}

部分執行結果

main --- 0
main --- 1
main --- 2
main --- 3
main --- 4
main --- 5
main --- 6
main --- 7
main --- 8
main --- 9
main --- 10
main --- 11
main --- 12
main --- 13
main --- 14
main --- 15
main --- 16
main --- 17
main --- 18
main --- 19
main --- 20
main --- 21
main --- 22
main --- 23
main --- 24
main --- 25
main --- 26
main --- 27
main --- 28
main --- 29
main --- 30
main --- 31
main --- 32
Thread-0 *** 0
Thread-0 *** 1
Thread-0 *** 2
Thread-0 *** 3
Thread-0 *** 4
Thread-0 *** 5
Thread-0 *** 6
Thread-0 *** 7
Thread-0 *** 8
Thread-0 *** 9
Thread-0 *** 10
Thread-0 *** 11
Thread-0 *** 12
Thread-0 *** 13
Thread-0 *** 14
main --- 33
main --- 34
main --- 35
main --- 36
main --- 37

可以看出

  • 執行緒啟動不一定立即執行,需要等待CPU排程
  • 多個執行緒交替執行,同一時刻只有一個執行緒執行
  • 每次執行結果不同,執行時隨機的
使用多執行緒下載檔案

實現多執行緒下載網路圖片到本地,使用commons-io包進行下載。

新增maven依賴

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

編寫工具類

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * 檔案下載工具類
 */
public class DownloadUtil {
    /**
     * 下載網路圖片到本地
     * @param url
     * @param fileName
     * @return
     */
    public static void download(String url, String fileName) {
        try {
            // 下載檔案放到本地
            FileUtils.copyURLToFile(new URL(url), new File(fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

建立自己的執行緒類並編寫測試程式碼

import java.io.File;

// 編寫自己的執行緒類繼承Thread類
public class MyThread extends Thread {
    private String url; // 網路圖片地址
    private String fileName; // 檔名

    // 通過有參構造傳url地址和檔名
    public MyThread(String url, String fileName) {
        this.url = url;
        this.fileName = fileName;
    }

    // 重寫父類的run方法
    @Override
    public void run() {
        // 呼叫寫好的工具類,下載檔案到本地
        DownloadUtil.download(url, fileName);
        System.out.println(fileName + " 下載完畢!");
    }

    // main方法測試
    public static void main(String[] args) {
        // 建立三個執行緒
        MyThread myThread1 = new MyThread("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1877714368,4013968043&fm=26&gp=0.jpg", "E:" + File.separator + "1.jpg");
        MyThread myThread2 = new MyThread("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3824205454,170685433&fm=26&gp=0.jpg", "E:" + File.separator + "2.jpg");
        MyThread myThread3 = new MyThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1599658994385&di=5546e75c68f9ccb7b6e887525f979b38&imgtype=0&src=http%3A%2F%2Fwww.bizhidaquan.com%2Fd%2Ffile%2Fdongwu%2Fyesheng%2F2015-01-19%2F35cda34d5345e6806aec2a081ae2181b.jpg", "E:" + File.separator + "3.jpg");
        // 啟動執行緒
        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}

控制檯列印結果

E:\2.jpg 下載完畢!
E:\1.jpg 下載完畢!
E:\3.jpg 下載完畢!

在E盤可以找到對應的檔案

實現Runnable介面

程式碼實現

// 1. 實現Runnable介面
public class MyThread implements Runnable {
    // 2. 實現run()方法
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " *** " + i);
        }
    }

    // 測試程式碼
    public static void main(String[] args) {
        // 3. 建立例項化物件
        MyThread myThread = new MyThread();
        // 4. 建立Thread類的例項化物件,將自定義執行緒類的例項化物件作為引數
        Thread thread = new Thread(myThread);
        // 5. 呼叫Thread類的start()方法啟動執行緒
        thread.start();

        // 主執行緒中的其他操作
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " --- " + i);
        }
    }
}

執行結果和繼承Thread實現是一樣的

main --- 45
main --- 46
Thread-0 *** 0
Thread-0 *** 1
Thread-0 *** 2
Thread-0 *** 3
Thread-0 *** 4
Thread-0 *** 5
Thread-0 *** 6
Thread-0 *** 7
Thread-0 *** 8
Thread-0 *** 9
Thread-0 *** 10
Thread-0 *** 11
Thread-0 *** 12
Thread-0 *** 13
Thread-0 *** 14
Thread-0 *** 15
Thread-0 *** 16
Thread-0 *** 17
Thread-0 *** 18
Thread-0 *** 19
Thread-0 *** 20
Thread-0 *** 21
Thread-0 *** 22
Thread-0 *** 23
Thread-0 *** 24
Thread-0 *** 25
Thread-0 *** 26
Thread-0 *** 27
Thread-0 *** 28
Thread-0 *** 29
Thread-0 *** 30
Thread-0 *** 31
Thread-0 *** 32
Thread-0 *** 33
Thread-0 *** 34
Thread-0 *** 35
Thread-0 *** 36
Thread-0 *** 37
Thread-0 *** 38
Thread-0 *** 39
Thread-0 *** 40
Thread-0 *** 41
Thread-0 *** 42
Thread-0 *** 43
Thread-0 *** 44
Thread-0 *** 45
Thread-0 *** 46
Thread-0 *** 47
Thread-0 *** 48
Thread-0 *** 49
Thread-0 *** 50
Thread-0 *** 51
Thread-0 *** 52
Thread-0 *** 53
main --- 47
main --- 48
main --- 49
main --- 50
main --- 51
main --- 52
main --- 53
main --- 54
實現Runnable介面建立多執行緒實際上是靜態代理的實現

Runnable是共同的介面,Thread是代理類,自己實現的MyThread類是被代理類。

呼叫Thread的有參構造時,將真實物件(MyThread的例項化物件)傳給代理物件

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

在Thread類的init()方法中,把target值賦給了Thread的屬性target,也就是改引用指向了MyThread類的例項化物件。

this.target = target;

在Thread的run()方法中又呼叫了MyThread例項化物件的run()方法

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}
多個執行緒同時操作同一個物件
// 模擬買票
public class MyThread implements Runnable {
    private int num = 10; // 總票數

    @Override
    public void run() {
        while (true) {
            // 如果無票,退出迴圈
            if (num <= 0) {
                break;
            }
            try {
                // 模擬延時
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " --> 買到了第" + this.num-- + "張票");
        }
    }

    // 測試程式碼
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 多個執行緒操作同一個物件
        new Thread(myThread, "張三").start();
        new Thread(myThread, "李四").start();
        new Thread(myThread, "王五").start();
    }
}

執行結果

張三 --> 買到了第10張票
李四 --> 買到了第9張票
王五 --> 買到了第8張票
李四 --> 買到了第7張票
張三 --> 買到了第7張票
王五 --> 買到了第6張票
李四 --> 買到了第5張票
張三 --> 買到了第4張票
王五 --> 買到了第3張票
李四 --> 買到了第2張票
張三 --> 買到了第1張票
王五 --> 買到了第0張票

發現第7張票同時被兩個人買到了,王五買到了第0張票

由此可見,多個執行緒同時操作同一資源時,執行緒是不安全的

龜兔賽跑

程式碼實現

// 賽道是公用的
public class Race implements Runnable {
    private String winner; // 存放獲勝者

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            // 如果獲勝者不為空,說明比賽結束
            if (this.winner != null) {
                break;
            }

            // 模擬兔子睡覺
            if ("兔子".equals(Thread.currentThread().getName()) && i % 50 == 0) {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println(Thread.currentThread().getName() + " --> 跑了" + i + "米");

            // 如果超過100米,當前執行緒為獲勝者
            if (i >= 100) {
                this.winner = Thread.currentThread().getName();
                System.out.println("獲勝者是 --> " + this.winner);
            }
        }
    }

    // 測試程式碼
    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race, "兔子").start();
        new Thread(race, "烏龜").start();
    }
}

執行結果

烏龜 --> 跑了0米
烏龜 --> 跑了1米
烏龜 --> 跑了2米
烏龜 --> 跑了3米
烏龜 --> 跑了4米
烏龜 --> 跑了5米
烏龜 --> 跑了6米
烏龜 --> 跑了7米
烏龜 --> 跑了8米
烏龜 --> 跑了9米
烏龜 --> 跑了10米
烏龜 --> 跑了11米
烏龜 --> 跑了12米
烏龜 --> 跑了13米
烏龜 --> 跑了14米
烏龜 --> 跑了15米
烏龜 --> 跑了16米
烏龜 --> 跑了17米
烏龜 --> 跑了18米
烏龜 --> 跑了19米
烏龜 --> 跑了20米
烏龜 --> 跑了21米
烏龜 --> 跑了22米
烏龜 --> 跑了23米
烏龜 --> 跑了24米
烏龜 --> 跑了25米
烏龜 --> 跑了26米
烏龜 --> 跑了27米
烏龜 --> 跑了28米
烏龜 --> 跑了29米
烏龜 --> 跑了30米
烏龜 --> 跑了31米
烏龜 --> 跑了32米
烏龜 --> 跑了33米
烏龜 --> 跑了34米
烏龜 --> 跑了35米
烏龜 --> 跑了36米
烏龜 --> 跑了37米
烏龜 --> 跑了38米
烏龜 --> 跑了39米
烏龜 --> 跑了40米
烏龜 --> 跑了41米
烏龜 --> 跑了42米
烏龜 --> 跑了43米
烏龜 --> 跑了44米
烏龜 --> 跑了45米
烏龜 --> 跑了46米
烏龜 --> 跑了47米
烏龜 --> 跑了48米
烏龜 --> 跑了49米
烏龜 --> 跑了50米
烏龜 --> 跑了51米
烏龜 --> 跑了52米
烏龜 --> 跑了53米
烏龜 --> 跑了54米
烏龜 --> 跑了55米
烏龜 --> 跑了56米
烏龜 --> 跑了57米
烏龜 --> 跑了58米
烏龜 --> 跑了59米
烏龜 --> 跑了60米
烏龜 --> 跑了61米
烏龜 --> 跑了62米
烏龜 --> 跑了63米
烏龜 --> 跑了64米
烏龜 --> 跑了65米
烏龜 --> 跑了66米
烏龜 --> 跑了67米
烏龜 --> 跑了68米
兔子 --> 跑了0米
兔子 --> 跑了1米
兔子 --> 跑了2米
兔子 --> 跑了3米
兔子 --> 跑了4米
兔子 --> 跑了5米
兔子 --> 跑了6米
兔子 --> 跑了7米
兔子 --> 跑了8米
兔子 --> 跑了9米
兔子 --> 跑了10米
兔子 --> 跑了11米
兔子 --> 跑了12米
兔子 --> 跑了13米
兔子 --> 跑了14米
兔子 --> 跑了15米
兔子 --> 跑了16米
兔子 --> 跑了17米
兔子 --> 跑了18米
兔子 --> 跑了19米
兔子 --> 跑了20米
兔子 --> 跑了21米
兔子 --> 跑了22米
兔子 --> 跑了23米
兔子 --> 跑了24米
兔子 --> 跑了25米
兔子 --> 跑了26米
兔子 --> 跑了27米
兔子 --> 跑了28米
兔子 --> 跑了29米
兔子 --> 跑了30米
兔子 --> 跑了31米
兔子 --> 跑了32米
兔子 --> 跑了33米
兔子 --> 跑了34米
兔子 --> 跑了35米
兔子 --> 跑了36米
兔子 --> 跑了37米
兔子 --> 跑了38米
兔子 --> 跑了39米
兔子 --> 跑了40米
兔子 --> 跑了41米
兔子 --> 跑了42米
兔子 --> 跑了43米
兔子 --> 跑了44米
兔子 --> 跑了45米
兔子 --> 跑了46米
兔子 --> 跑了47米
兔子 --> 跑了48米
兔子 --> 跑了49米
烏龜 --> 跑了69米
烏龜 --> 跑了70米
烏龜 --> 跑了71米
烏龜 --> 跑了72米
烏龜 --> 跑了73米
烏龜 --> 跑了74米
烏龜 --> 跑了75米
烏龜 --> 跑了76米
烏龜 --> 跑了77米
烏龜 --> 跑了78米
烏龜 --> 跑了79米
烏龜 --> 跑了80米
烏龜 --> 跑了81米
烏龜 --> 跑了82米
烏龜 --> 跑了83米
烏龜 --> 跑了84米
烏龜 --> 跑了85米
烏龜 --> 跑了86米
烏龜 --> 跑了87米
烏龜 --> 跑了88米
烏龜 --> 跑了89米
烏龜 --> 跑了90米
烏龜 --> 跑了91米
烏龜 --> 跑了92米
烏龜 --> 跑了93米
烏龜 --> 跑了94米
烏龜 --> 跑了95米
烏龜 --> 跑了96米
烏龜 --> 跑了97米
烏龜 --> 跑了98米
烏龜 --> 跑了99米
烏龜 --> 跑了100米
獲勝者是 --> 烏龜
兔子 --> 跑了50米

實現Callable介面

程式碼實現

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 實現Callable介面,返回值型別String
public class MyCallable implements Callable<String> {
    // 2. 實現call()方法
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " --- " + i);
        }
        return Thread.currentThread().getName();
    }

    // 測試程式碼
    public static void main(String[] args) {
        // 3. 建立MyCallable例項化物件
        MyCallable myCallable = new MyCallable();
        // 4. 將MyCallable例項化物件包裝成FutureTask
        // 繼承關係: FutureTask實現了RunnableFuture介面,RunnableFuture介面繼承了Runnable介面
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        // 5. 建立Thread例項化物件
        Thread thread = new Thread(futureTask);
        // 6. 呼叫start()方法啟動執行緒
        thread.start();
        // 7. 獲取返回值
        String result = null;
        try {
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("call()返回值:" + result);
    }
}

FutureTask實現了RunnableFuture介面,RunnableFuture介面繼承了Runnable介面,傳遞給Thread類構造方法的依然是Runnable的實現類。

因為FutureTask實現了Runnable介面,那麼FutureTask必定實現了run()方法,並在run()方法中呼叫了Callable實現類中的call()方法,本質還是實現Runnable介面的run()方法作為執行緒體,呼叫Thread類的start()方法啟動執行緒

實現Callable和前兩種方式的不同之處在於:

  • call()方法有返回值
  • call()方法可以丟擲異常

總結

以上三種方式實際上都是通過Thread類建立並啟動執行緒,啟動執行緒的方式只有一種,就是呼叫Thread類的start()方法