Java 多執行緒:基礎

Java 多執行緒:基礎

作者:Grey

原文地址:

部落格園:Java 多執行緒:基礎

CSDN:Java 多執行緒:基礎

順序、並行與並發

順序(sequential)用於表示多個操作『依次』處理。比如把十個操作交給一個人處理時,這個人要一個一個地按順序來處理。

並行(parallel)用於表示多個操作『同時』處理」。比如十個操作分給兩個人處理時,這兩個人會並行來處理。

並發(concurrent)相對於順序和並行來說比較抽象,用於表示『將一個操作分割成多個部分並且允許無序處理』。比如將十個操作分成相對獨立的兩類,這樣便可以開始並發處理了。如果一個人來處理,這個人就是順序處理分開的並發操作,而如果是兩個人。這兩個人就可以並行處理同一操作。

如果 CPU 只有一個,那麼並發處理就是順序執行的,而如果有多個 CPU,那麼並發處理就可能會並行運行。

image

什麼是程式,進程,執行緒和協程

程式是電腦的可執行文件;

進程是電腦資源分配的基本單位;

執行緒是資源調度執行的基本單位,也可以說:執行緒是一個程式裡面不同的執行路徑,多個執行緒共享進程中的資源;

協程是一種用戶態的輕量級執行緒,協程的調度完全由用戶控制。協程擁有自己的暫存器上下文和棧。協程調度切換時,將暫存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的暫存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變數,所以上下文的切換非常快。協程在子程式內部可中斷的,然後轉而執行別的子程式,在適當的時候再返回來接著執行。

協程的特點在於是一個執行緒執行,那和多執行緒比,協程有如下優勢:

優勢一:極高的執行效率:因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的性能優勢就越明顯;

優勢二:不需要多執行緒的鎖機制:因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。

注意:協程避免了無意義的調度,由此可以提高性能,但是程式設計師必須自己承擔調度的責任,同時,協程也失去了標準執行緒使用多 CPU 的能力。

一個簡單的協程示例, 程式碼如下:

註:

  1. 需要引入quasar-core依賴包。

  2. 如果在 Java SE 16 以及更高版本上運行,需要增加如下參數

--add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
package git.snippets.juc;

import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.channels.Channel;
import co.paralleluniverse.strands.channels.Channels;

import java.util.concurrent.ExecutionException;

/**
 * Java協程示例
 * JDK 11 ~ JDK 15 沒問題,
 *
 * JDK 16 開始,需要增加如下參數
 *
 * --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
 *
 * @since jdk11
 * 需要引入:quasar-core依賴包
 */
public class FiberSample {
    private static void printer(Channel<Integer> in) throws SuspendExecution, InterruptedException {
        Integer v;
        while ((v = in.receive()) != null) {
            System.out.println(v);
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution {
        //定義兩個Channel
        try (Channel<Integer> naturals = Channels.newChannel(-1); Channel<Integer> squares = Channels.newChannel(-1)) {

            //運行兩個Fiber實現.
            new Fiber(() -> {
                for (int i = 0; i < 10; i++) {
                    naturals.send(i);
                }
                naturals.close();
            }).start();

            new Fiber(() -> {
                Integer v;
                while ((v = naturals.receive()) != null) {
                    squares.send(v * v);
                }
                squares.close();
            }).start();

            printer(squares);
        }

    }
}

執行緒和進程的關係

執行緒就是輕量級進程,是程式執行的最小單位。

多進程的方式也可以實現並發,為什麼我們要使用多執行緒?主要是基於以下兩方面的原因:

  1. 共享資源在執行緒間的通訊比較容易。

  2. 執行緒開銷更小。

進程和執行緒的區別

進程是一個獨立的運行環境,而執行緒是在進程中執行的一個任務。他們兩個本質的區別在於是否單獨佔有記憶體地址空間及其它系統資源

進程是作業系統進行資源分配的基本單位,而執行緒是作業系統進行調度的基本單位,即 CPU 分配時間的單位。

進程單獨佔有一定的記憶體地址空間,所以進程間存在記憶體隔離,數據是分開的,數據共享複雜但是同步簡單,各個進程之間互不干擾;而執行緒共享所屬進程佔有的記憶體地址空間和資源,數據共享簡單,但是同步複雜。

進程單獨佔有一定的記憶體地址空間,一個進程出現問題不會影響其他進程,不影響主程式的穩定性,可靠性高;一個執行緒崩潰可能影響整個程式的穩定性,可靠性較低。

進程的創建和銷毀不僅需要保存暫存器和棧資訊,還需要資源的分配回收以及頁調度,開銷較大;執行緒只需要保存暫存器和棧資訊,開銷較小。

多執行緒訪問成員變數與局部變數

類變數(類裡面 static 修飾的變數)保存在「方法區」

實例變數(類裡面的普通變數)保存在「堆」

局部變數(方法里聲明的變數)「虛擬機棧」

「方法區」和「堆」都屬於執行緒共享數據區,「虛擬機棧」屬於執行緒私有數據區。

image

因此,局部變數是不能多個執行緒共享的,而類變數和實例變數是可以多個執行緒共享的。事實上,在 Java 中,多執行緒間進行通訊的唯一途徑就是通過類變數和實例變數。也就是說,如果一段多執行緒程式中如果沒有類變數和實例變數,那麼這段多執行緒程式就一定是執行緒安全的。

開發過程中,為了解決執行緒安全問題,有如下角度可以考慮:

第一種方案:盡量使用局部變數,代替實例變數和靜態變數。

第二種方案:如果必須是實例變數,那麼可以考慮創建多個對象,這樣實例變數的記憶體就不共享了( 1 個執行緒對應 1 個對象,100 個對象對應 100 個對象,對象不共享,就沒有數據安全問題了)

第三種方案:如果不使用局部變數。對象也不能創建多個。這個時候,就只能選擇syncharonized了。

執行緒的共享資源和獨有資源

其中共享資源包括:

  • 進程程式碼段

  • 進程的公有數據

  • 進程打開的文件描述符、訊號的處理器、進程的當前目錄和進程用戶 ID 與進程組 ID。

獨有資源包括:

  • 執行緒ID:每個執行緒都有自己的執行緒 ID,這個 ID 在本進程中是唯一的。進程用此來標識執行緒。

  • 暫存器組的值:由於執行緒間是並發運行的,每個執行緒有自己不同的運行線索,當從一個執行緒切換到另一個執行緒上時,必須將原有的執行緒的暫存器集合的狀態保存,以便將來該執行緒在被重新切換到時能得以恢復。

  • 執行緒的堆棧:堆棧是保證執行緒獨立運行所必須的。執行緒函數可以調用函數,而被調用函數中又是可以層層嵌套的,所以執行緒必須擁有自己的函數堆棧, 使得函數調用可以正常執行,不受其他執行緒的影響。

  • 錯誤返回碼:由於同一個進程中有很多個執行緒在同時運行,可能某個執行緒進行系統調用後設置了 err no 值,而在該執行緒還沒有處理這個錯誤,另外一個執行緒就在此時被調度器投入運行,這樣錯誤值就有可能被修改。所以,不同的執行緒應該擁有自己的錯誤返回碼變數。

  • 執行緒的訊號屏蔽碼:由於每個執行緒所感興趣的訊號不同,所以執行緒的訊號屏蔽碼應該由執行緒自己管理。但所有的執行緒都共享同樣的訊號處理器。

  • 執行緒的優先順序:由於執行緒需要像進程那樣能夠被調度,那麼就必須要有可供調度使用的參數,這個參數就是執行緒的優先順序。

什麼是執行緒切換?

從底層角度上看,CPU 主要由如下三部分組成,分別是:

  • ALU: 計算單元

  • Registers: 暫存器組

  • PC:存儲到底執行到哪條指令

T1 執行緒在執行的時候,將 T1 執行緒的指令放在 PC,數據放在 Registers,假設此時要切換成 T2 線 程,T1 執行緒的指令和數據放 cache,然後把 T2 執行緒的指令放 PC,數據放 Registers,執行 T2 執行緒即可。

以上的整個過程是通過作業系統來調度的,且執行緒的調度是要消耗資源的,所以,執行緒不是設置越多越好。

示例:

單執行緒和多執行緒來累加 1 億個數。 示例程式碼如下

package git.snippets.juc;

import java.text.DecimalFormat;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * 多執行緒求1億個Double類型的數據
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2021/7/7
 * @since
 */
public class CountSum {
    private static final double[] NUMS = new double[1_0000_0000];
    private static final Random R = new Random();
    private static final DecimalFormat FORMAT = new DecimalFormat("0.00");
    static {
        for (int i = 0; i < NUMS.length; i++) {
            NUMS[i] = R.nextDouble();
        }
    }
    static double result1 = 0.0, result2 = 0.0, result = 0.0;
    public static void rand() {
        for (int i = 0; i < NUMS.length; i++) {
            NUMS[i] = R.nextDouble();
        }
    }

    /**
     * 單執行緒計算一億個Double類型的數據之和
     *
     * @return
     */
    public static String m1() {
        long start = System.currentTimeMillis();
        double result = 0.0;
        for (double num : NUMS) {
            result += num;
        }
        long end = System.currentTimeMillis();
        System.out.println("計算1億個隨機Double類型數據之和[單執行緒], 結果是:result = " + FORMAT.format(result) + " 耗時 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(result));
    }

    /**
     * 兩個執行緒計算一億個Double類型的數據之和
     *
     * @return
     */
    private static String m2() throws Exception {
        long start = System.currentTimeMillis();
        result1 = 0.0;
        result2 = 0.0;
        int len = (NUMS.length >> 1);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < len; i++) {
                result1 += NUMS[i];
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = len; i < NUMS.length; i++) {
                result2 += NUMS[i];
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        result = result1 + result2;
        long end = System.currentTimeMillis();
        System.out.println("計算1億個隨機Double類型數據之和[2個執行緒], 結果是:result = " + FORMAT.format(result) + " 耗時 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(result));
    }

    /**
     * 10個執行緒計算一億個Double類型的數據之和
     *
     * @return
     */
    private static String m3() throws Exception {
        long start = System.currentTimeMillis();
        final int threadCount = 10;
        Thread[] threads = new Thread[threadCount];
        double[] results = new double[threadCount];

        final int segmentCount = NUMS.length / threadCount;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            int m = i;
            threads[i] = new Thread(() -> {
                for (int j = m * segmentCount; j < (m + 1) * segmentCount && j < NUMS.length; j++) {
                    results[m] += NUMS[j];
                }
                latch.countDown();
            });

        }
        double resultM3 = 0.0;

        for (Thread t : threads) {
            t.start();
        }
        latch.await();
        for (double v : results) {
            resultM3 += v;
        }

        long end = System.currentTimeMillis();
        System.out.println("計算1億個隨機Double類型數據之和[10個執行緒], 結果是:result = " + FORMAT.format(resultM3) + " 耗時 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(resultM3));
    }

    public static void main(String[] args) throws Exception {
        int testCount = 10;
        boolean correct = true;
        for (int i = 0; i < testCount; i++) {
            rand();
            String s = m1();
            String s1 = m2();
            String s2 = m3();
            if (!s1.equals(s2) || !s1.equals(s)) {
                System.out.println("oops!");
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s);
                correct = false;
                break;
            }
        }
        if (correct) {
            System.out.println("test finished");
        }
    }
}

運行結果

……
計算1億個隨機Double類型數據之和[單執行緒], 結果是:result = 49998124.71 耗時 : 114ms
計算1億個隨機Double類型數據之和[2個執行緒], 結果是:result = 49998124.71 耗時 : 53ms
計算1億個隨機Double類型數據之和[10個執行緒], 結果是:result = 49998124.71 耗時 : 54ms

計算1億個隨機Double類型數據之和[單執行緒], 結果是:result = 50000309.80 耗時 : 102ms
計算1億個隨機Double類型數據之和[2個執行緒], 結果是:result = 50000309.80 耗時 : 53ms
計算1億個隨機Double類型數據之和[10個執行緒], 結果是:result = 50000309.80 耗時 : 35ms

計算1億個隨機Double類型數據之和[單執行緒], 結果是:result = 50001943.57 耗時 : 108ms
計算1億個隨機Double類型數據之和[2個執行緒], 結果是:result = 50001943.57 耗時 : 58ms
計算1億個隨機Double類型數據之和[10個執行緒], 結果是:result = 50001943.57 耗時 : 41ms

計算1億個隨機Double類型數據之和[單執行緒], 結果是:result = 49997176.44 耗時 : 102ms
計算1億個隨機Double類型數據之和[2個執行緒], 結果是:result = 49997176.44 耗時 : 53ms
計算1億個隨機Double類型數據之和[10個執行緒], 結果是:result = 49997176.44 耗時 : 29ms
……

可以看到結果中,創建 10 個執行緒 不一定會比創建 2 個執行緒要執行更快。

單核 CPU 設定多執行緒是否有意義

有意義,因為執行緒的操作中可能有不消耗 CPU 的操作,比如:等待網路的傳輸,或者執行緒 sleep,此時就可以讓出 CPU 去執行其他執行緒。可以充分利用 CPU 資源。

工作執行緒數(執行緒池中執行緒數量)設多少合適

  • 和 CPU 的核數有關

  • 最好是通過壓測來評估。通過 profiler 性能分析工具 JProfiler,或者 Arthas

  • 公式

N = Ncpu * Ucpu * (1 + W/C)

其中:

  • Ncpu 是處理器的核的數目,可以通過Runtime.getRuntime().availableProcessors() 得到

  • Ucpu 是期望的 CPU 利用率(該值應該介於 0 和 1 之間)

  • W/C 是等待時間和計算時間的比率。

更深入的分析,可以參考這篇文章

一個 Hello World 程式運行的時候啟動了幾個執行緒

使用如下程式碼:

public class HowManyThreadHelloWorld {
    
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("\n執行緒:" + t.getName() + "\n");
        System.out.println("hello world!");

        for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
            Thread thread = entry.getKey();

            StackTraceElement[] stackTraceElements = entry.getValue();

            if (thread.equals(Thread.currentThread())) {
                continue;
            }

            System.out.println("\n執行緒: " + thread.getName() + "\n");
            for (StackTraceElement element : stackTraceElements) {
                System.out.println("\t" + element + "\n");
            }
        }
    }
}

在 Java SE 11 下執行,可以看到,有如下執行緒資訊

執行緒:main
執行緒: Reference Handler
執行緒: Signal Dispatcher
執行緒: Finalizer
執行緒: Common-Cleaner
執行緒: Attach Listener

在 Java SE 8 下執行,有如下執行緒資訊

執行緒:main
執行緒: Finalizer
執行緒: Attach Listener
執行緒: Signal Dispatcher
執行緒: Reference Handler

其中

Reference Handler:處理引用對象本身的垃圾回收

Finalizer:處理用戶的 Finalizer 方法

Signal Dispatcher:外部 jvm 命令的轉發器

Attach Listener: jvm 提供一種 jvm 進程間通訊的能力,能讓一個進程傳命令給另外一個進程

Common-Cleaner: 該執行緒是 Java SE 9 之後新增的守護執行緒,用來更高效的處理垃圾回收

Java 中創建執行緒的方式

  1. 繼承Thread類,重寫run方法。

  2. 實現Runnable介面,實現run方法,這比方式 1 更好,因為一個類實現了Runnable以後,還可以繼承其他類

  3. 通過執行緒池創建。

  4. 在需要返回值的時候,可以通過CallableFutureFutureTask來創建。

示例程式碼如下

package git.snippets.juc;

import java.util.concurrent.*;

/**
 * 創建執行緒的方式
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2021/7/7
 * @since 1.8
 */
public class HelloThread {
    public static void main(String[] args) throws Exception {
        MyFirstThread t1 = new MyFirstThread();
        Thread t2 = new Thread(new MySecondThread());
        Thread t3 = new Thread(new FutureTask<>(new CallableThreadTest()));
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> System.out.println("方式3:使用執行緒池來創建執行緒。"));
        t1.start();
        t2.start();
        t3.start();
        executor.shutdown();
        boolean b = executor.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(b ? "停止成功" : "停止失敗");
    }

    static class MyFirstThread extends Thread {
        @Override
        public void run() {
            System.out.println("方式1:繼承Thread類並重寫run方法來創建執行緒");
        }
    }

    /**
     * 方式二, 實現Runnable介面來創建執行緒
     */
    static class MySecondThread implements Runnable {

        @Override
        public void run() {
            System.out.println("方式2:實現Runnable方式來創建執行緒");
        }
    }

    static class CallableThreadTest implements Callable<Integer> {
        @Override
        public Integer call() {
            int i;
            for (i = 0; i < 10; i++) {
                i++;
            }
            System.out.println("方式4,實現Callable介面方式來創建有返回值的執行緒,返回值是:" + i);
            return i;
        }
    }
}

執行緒狀態和切換

NEW:執行緒剛剛創建,還沒有啟動,New Thread 的時候,還沒有調用start方法時候,就是這個狀態

RUNNABLE:可運行狀態,由執行緒調度器可以安排執行,包括以下兩種情況:

  • READY

  • RUNNING

READY 和 RUNNING 通過yield方法來切換

WAITING:等待被喚醒

TIMED_WAITING:隔一段時間後自動喚醒

BLOCKED:被阻塞,正在等待鎖,只有在synchronized的時候在會進入BLOCKED狀態

TERMINATED:執行緒執行完畢後,是這個狀態

各個執行緒狀態切換如下

執行緒狀態

執行緒基本操作

sleep:當前執行緒睡一段時間

yield:這是一個靜態方法,一旦執行,它會使當前執行緒讓出一下 CPU。但要注意,讓出 CPU 並不表示當前執行緒不執行了。當前執行緒在讓出 CPU 後,還會進行 CPU 資源的爭奪,但是是否能夠再次被分配到就不一定了。

join:等待另外一個執行緒的結束,當前執行緒才會運行,示例程式碼如下:

public class ThreadBasicOperation {
    static volatile int sum = 0;

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
        });
        t.start();
        // join 方法表示主執行緒願意等待子執行緒執行完畢後才繼續執行
        // 如果不使用join方法,那麼sum輸出的可能是一個很小的值,因為還沒等子執行緒
        // 執行完畢後,主執行緒就已經執行了列印sum的操作
        t.join();
        System.out.println(sum);
    }
}

interrupt:打斷執行緒執行,有三個方法。

// 打斷某個執行緒(設置標誌位)
interrupt()
// 查詢某執行緒是否被打斷過(查詢標誌位)
isInterrupted()
// 查詢當前執行緒是否被打斷過,並重置打斷標誌位
Thread.interrupted()

示例程式碼如下

package git.snippets.juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * interrupt示例
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @since 1.8
 */
public class ThreadInterrupt {
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (; ; ) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("t thread interrupted");
                    System.out.println(Thread.currentThread().isInterrupted());
                    break;
                }
            }
        });
        t.start();
        TimeUnit.SECONDS.sleep(3);
        t.interrupt();

        Thread t2 = new Thread(() -> {
            for (; ; ) {
                if (Thread.interrupted()) {
                    System.out.println("t2 thread interrupted");
                    // Thread.interrupted()會將執行緒中斷狀態置為false
                    System.out.println(Thread.currentThread().isInterrupted());
                    break;
                }
            }
        });
        t2.start();
        TimeUnit.SECONDS.sleep(3);
        t2.interrupt();

        Thread t3 = new Thread(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("t3 interrupted");
                // 如果不加上這一句,那麼Thread.currentThread().isInterrupted()將會都是false,因為在捕捉到InterruptedException異常的時候就會自動的中斷標誌置為了false
                Thread.currentThread().interrupt();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });

        t3.start();
        TimeUnit.SECONDS.sleep(3);
        t3.interrupt();

        final Object o = new Object();
        Thread t4 = new Thread(() -> {
            synchronized (o) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    System.out.println("t4 interrupted!");
                    Thread.currentThread().interrupt();
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t4.start();
        TimeUnit.SECONDS.sleep(10);
        t4.interrupt();

        Thread t5 = new Thread(() -> {
            synchronized (o) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t5.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t6 = new Thread(() -> {
            synchronized (o) {

            }
            System.out.println("t6 finished");
        });
        t6.start();
        t6.interrupt();


        Thread t7 = new Thread(() -> {
            LOCK.lock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
            System.out.println("t7 end");
        });
        t7.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t8 = new Thread(() -> {
            LOCK.lock();
            try {
            } finally {
                LOCK.unlock();
            }
            System.out.println("t8 end");
        });
        t8.start();
        TimeUnit.SECONDS.sleep(1);
        t8.interrupt();

        Thread t9 = new Thread(() -> {
            LOCK.lock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
            System.out.println("t7 end");
        });
        t9.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t10 = new Thread(() -> {
            System.out.println("t10 start");
            try {
                LOCK.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("t10 interrupted");
            } finally {
                LOCK.unlock();
            }
            System.out.println("t8 end");
        });
        t10.start();
        TimeUnit.SECONDS.sleep(1);
        t10.interrupt();

    }
}

關於執行緒的 start 方法

問題1:反覆調用同一個執行緒的start()方法是否可行?

問題2:假如一個執行緒執行完畢(此時處於 TERMINATED 狀態),再次調用這個執行緒的start()方法是否可行?

兩個問題的答案都是不可行,在調用一次start()之後,threadStatus的值會改變(threadStatus !=0),此時再次調用start()方法會拋出IllegalThreadStateException異常。

如何結束一個執行緒

不推薦的方式

  • stop方法

  • suspend結合resume方法

以上兩種方式都不建議使用, 因為會釋放所有的鎖, 所以容易產生數據不一致的問題。

優雅的方式

  • 如果不依賴循環的具體次數或者中間狀態, 可以通過設置標誌位的方式來控制。

  • 如果要依賴循環的具體次數或者中間狀態, 則可以用interrupt方法。

上述四種方式的示例程式碼如下:

package git.snippets.juc;

import java.util.concurrent.TimeUnit;

/**
 * 如何結束一個執行緒
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @since 1.8
 */
public class ThreadFinished {
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 推薦方式:設置標誌位
        useVolatile();
        // 推薦方式:使用interrupt
        useInterrupt();
        // 使用stop方法來結束執行緒,不推薦
        useStop();
        // 使用suspend/resume方法來結束執行緒,不推薦
        useResumeAndSuspend();
    }

    private static void useResumeAndSuspend() throws InterruptedException {
        Thread t2 = new Thread(() -> {
            System.out.println("t2 start");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                // e.printStackTrace();
            }
            System.out.println("t2 finished");
        });
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        t2.suspend();
        TimeUnit.SECONDS.sleep(1);
        t2.resume();
    }

    private static void useStop() throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("t start");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                // e.printStackTrace();
            }
            System.out.println("t finished");
        });
        t.start();
        TimeUnit.SECONDS.sleep(1);
        t.stop();
    }

    private static void useInterrupt() throws InterruptedException {
        Thread t4 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {

            }
            System.out.println("t4 end");
        });
        t4.start();
        TimeUnit.SECONDS.sleep(1);
        t4.interrupt();
    }

    private static void useVolatile() throws InterruptedException {
        Thread t3 = new Thread(() -> {
            long i = 0L;
            while (flag) {
                i++;
            }
            System.out.println("count sum i = " + i);
        });
        t3.start();
        TimeUnit.SECONDS.sleep(1);
        flag = false;
    }
}

說明

本文涉及到的所有程式碼和圖例

圖例

程式碼

更多內容見:Java 多執行緒

參考資料

工作執行緒數究竟要設置為多少 | 架構師之路

實戰Java高並發程式設計(第2版)

深入淺出Java多執行緒

多執行緒與高並發-馬士兵

Java並發編程實戰

進程、執行緒、協程三者之間的聯繫與區別

Java如何實現協程

圖解Java多執行緒設計模式