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多線程設計模式