Java中的多线程

Java中的多线程概念

🔀 并发和并行

  • 并行:多核 CPU 上的多任务处理,多个任务在同一时间真正地同时执行。
  • 并发:单核 CPU 上的多任务处理,多个任务在同一时间段内交替执行,通过时间片轮转实现交替执行。
并行和并发的区别

🔄 同步和异步

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

🧵 进程和线程

  • 进程:是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程:是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。

🧵 进程和线程之间的关系

  • 线程在进程下进行
  • 进程之间不会相互影响,主线程结束将会导致整个进程结束
  • 不同的进程数据很难共享
  • 同进程下的不同线程之间数据很容易共享
  • 进程使用内存地址可以限定使用量

🧵 Java中的线程和操作系统的线程的关系

在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。

所以本质上 java 程序创建的线程,就是和操作系统线程是一样的,是 1 对 1 的线程模型。


🧵 线程的上下文切换

并发其实是一个 CPU 来应付多个线程。CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用。

线程在执行过程中会有自己的运行条件和状态(也称上下文)。线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。

同时线程可以被多核调度。操作系统的调度器负责将线程分配给可用的 CPU 核心,从而实现并行处理。多核处理器提供了并行执行多个线程的能力。


🧵 线程安全

🧵 线程安全的定义

线程安全是指多个线程访问某一共享资源时,能够保证一致性和正确性,即无论线程如何交替执行,程序都能够产生预期的结果,且不会出现数据竞争或内存冲突

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作.一个操作或一系列操作要么全部执行成功,要么全部不执行,期间不会被其他线程干扰。在Java中使用了atomic包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和 synchronized 关键字来确保原子性

    • 原子类与锁:Java 提供了 java.util.concurrent.atomic 包中的原子类,如 AtomicIntegerAtomicLong ,来保证基本类型的操作具有原子性。此外,synchronized 关键字和 Lock 接口也可以用来确保操作的原子性。
    • CAS(Compare-And-Swap):Java 的原子类底层依赖于 CAS 操作来实现原子性。CAS 是一种硬件级的指令,它比较内存位置的当前值与给定的旧值,如果相等则将内存位置更新为新值,这一过程是原子的。CAS 可以避免传统锁机制带来的上下文切换开销。
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到

    • volatilevolatile 关键字是 Java 中用来保证可见性的轻量级同步机制。当一个变量被声明为 volatile 时,所有对该变量的读写操作都会直接从主内存中进行,从而确保变量对所有线程的可见性。
    • synchronizedsynchronized 关键字不仅可以保证代码块的原子性,还可以保证进入和退出 synchronized 块的线程能够看到块内变量的最新值。每次线程退出 synchronized 块时,都会将修改后的变量值刷新到主内存中,进入该块的线程则会从主内存中读取最新的值。
    • Java Memory Model(JMM):JMM 规定了共享变量在不同线程间的可见性和有序性规则。它定义了内存屏障的插入规则,确保在多线程环境下的代码执行顺序和内存可见性。
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

    • 指令重排序:为了提高性能,处理器和编译器可能会对指令进行重排序。尽管重排序不会影响单线程中的执行结果,但在多线程环境下可能会导致严重的问题。例如,经典的双重检查锁定(DCL)模式在没有正确同步的情况下,由于指令重排序可能导致对象尚未完全初始化就被另一个线程访问。
    • happens-before 原则:JMM 定义了 happens-before 规则,用于约束操作之间的有序性。如果一个操作 A happens-before 操作 B,那么 A 的结果对于 B 是可见的,且 A 的执行顺序在 B 之前。这为开发者提供了在多线程环境中控制操作顺序的手段。
    • 内存屏障volatile 变量的读写操作会在指令流中插入内存屏障,阻止特定的指令重排序。对于 volatile 变量的写操作,会在写操作前插入一个 StoreStore 屏障,防止写操作与之前的写操作重排序;在读操作之后插入一个 LoadLoad 屏障,防止读操作与之后的读操作重排序。
  • 活跃性

    • 死锁是指多个线程因为环形等待锁的关系而永远地阻塞下去。
      死锁示意图
- **活锁**:线程没有阻塞。当多个线程都在运行并且都在修改各自的状态,而其他线程又依赖这个状态,就导致任何一个线程都无法继续执行,只能重复着自身的动作,于是就发生了活锁。
活锁示意图
- **饥饿**:如果一个线程无其他异常却迟迟不能继续运行。 - 高优先级的线程一直在运行消耗 CPU,所有的低优先级线程一直处于等待; - 一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;

🧵 常见线程安全措施


🧵 线程的创建方式

📦 继承 Thread

创建一个类继承 Thread 类,并重写 run() 方法

run() 方法中定义了线程执行的具体任务。创建该类的实例后,通过调用 start() 方法启动线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
}

优缺点

🔌 实现 Runnable 接口

创建一个类实现 Runnable 接口,并重写 run() 方法,使用 Thread 类的构造函数传入 Runnable 对象,调用 start() 方法启动线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
}

优缺点

🔌 实现 Callable 接口与 FutureTask

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程执行的代码,这里返回一个整型结果
        return 1;
    }
}

public static void main(String[] args) {
    MyCallable task = new MyCallable();
    FutureTask<Integer> futureTask = new FutureTask<>(task);
    Thread t = new Thread(futureTask);
    t.start();
    try {
        Integer result = futureTask.get();  // 获取线程执行结果
        System.out.println("Result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

💡 使用线程池( ExecutorService

通过 ExecutorService 提交 RunnableCallable 任务,不直接创建和管理线程,适合管理大量并发任务。

 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
// 提交Runnable任务
class Task implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(10);  // 创建固定大小的线程池
    for (int i = 0; i < 10; i++) {
        executor.submit(new Task());  // 提交任务到线程池执行
    }
    executor.shutdown();  // 关闭线程池
}

//提交Callable任务
import java.util.concurrent.*;

class Task implements Callable<String> {
    @Override
    public String call() {
        return "Task executed by " + Thread.currentThread().getName();
    }
}

public class CallableExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);  // 创建固定大小的线程池
        Future<String>[] results = new Future[10];  // 存储任务的返回结果
        
        for (int i = 0; i < 10; i++) {
            results[i] = executor.submit(new Task());  // 提交 Callable 任务
        }
        
        // 获取任务的返回结果
        for (Future<String> result : results) {
            try {
                System.out.println(result.get());  // get() 方法会阻塞直到结果可用
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        executor.shutdown();  // 关闭线程池
    }
}

优缺点

提示

RunnableCallable


🔌 Runnable 接口 和 Callable 接口

📌 无返回值的 Runnable

1
2
3
public interface Runnable {
    public abstract void run();
}

执行完任务之后无法返回任何结果

📌 有返回值的 Callable

1
2
3
public interface Callable<V> {
    V call() throws Exception;
}

call() 方法返回的类型是一个 V 类型的泛型


📦 Future 接口和 FutureTask 实现类

1
2
3
4
5
6
7
8
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future 位于 java.util.concurrent 包下,是一个接口

1
public class FutureTask<V> implements RunnableFuture<V>

FutureTask 是唯一的实现类

构造器

1
2
3
4
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}

示例

 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
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);

// 创建一系列 Callable
Callable<Integer>[] tasks = new Callable[5];
for (int i = 0; i < tasks.length; i++) {
    final int index = i;
    tasks[i] = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            TimeUnit.SECONDS.sleep(index + 1);
            return (index + 1) * 100;
        }
    };
}

// 将 Callable 包装为 FutureTask,并提交到线程池
FutureTask<Integer>[] futureTasks = new FutureTask[tasks.length];
for (int i = 0; i < tasks.length; i++) {
    futureTasks[i] = new FutureTask<>(tasks[i]);
    executorService.submit(futureTasks[i]);
}

// 获取任务结果
for (int i = 0; i < futureTasks.length; i++) {
    System.out.println("Result of task" + (i + 1) + ": " + futureTasks[i].get());
}

// 关闭线程池
executorService.shutdown();

🔨 控制线程的常用方法

🧵 启动线程

在 Java 中,启动一个新的线程应该调用其 start() 方法,而不是直接调用 run() 方法。

当调用 start() 方法时,会启动一个新的线程,并让这个新线程调用 run() 方法。这样,run() 方法就在新的线程中运行,从而实现多线程并发。

如果直接调用 run() 方法,那么 run() 方法就在当前线程中以同步的方式运行,没有新的线程被创建,也就没有实现多线程的效果。

🧵 线程命名

🧵 线程休眠

🚀 线程优先执行

等待这个线程执行完才会轮到后续线程得到 cpu 的执行权

🧵 线程间通信

wait() 让当前线程释放锁并进入等待状态

notify() 唤醒一个等待的线程,具体唤醒哪个等待的线程是随机的,notifyAll() 唤醒所有等待的线程。

🧵 中断线程

响应中断示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void run() {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            // 执行任务
        }
    } catch (InterruptedException e) {
        // 线程被中断时的清理代码
    } finally {
        // 线程结束前的清理代码
    }
}

📌 让出时间片

yield() 方法用于暗示当前线程愿意放弃其当前的时间片,允许其他线程执行。它并不会使线程进入阻塞状态,线程依然处于 RUNNABLE 状态。但是它只是向线程调度器提出建议,调度器可能会忽略这个建议。具体行为取决于操作系统和JVM)的线程调度策略。(和 Thread.sleep(0) 功能相同)

🧵 设置线程优先级

🧵 守护线程

将此线程标记为守护线程。

守护线程:是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。

提示

sleep()wait() 的区别(面试题)


📊 线程的状态

状态转移图

可以使用Thread中的 getState() 方法获取状态

Java中的线程状态

📊 NEW(初始状态)

处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的 start() 方法。

 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
// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
    // threadStatus != 0 表示这个线程已经被启动过或已经结束了
    // 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    // 将这个线程添加到当前线程的线程组中
    group.add(this);

    // 声明一个变量,用于记录线程是否启动成功
    boolean started = false;
    try {
        // 使用native方法启动这个线程
        start0();
        // 如果没有抛出异常,那么started被设为true,表示线程启动成功
        started = true;
    } finally {
        // 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
        try {
            // 如果线程没有启动成功,就从线程组中移除这个线程
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            // 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
        }
    }
}

因此

  1. 反复调用同一个线程的 start() 方法不可行
  2. 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start() 方法不可行

在调用 start() 之后,threadStatus 的值会改变( threadStatus !=0 ),再次调用 start() 方法会抛出 IllegalThreadStateException 异常。

⚡ RUNNABLE(正在运行)

表示当前线程正在运行中。当调用线程的 start() 方法后,线程进入可运行状态。处于 RUNNABLE 状态的线程在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。

1
2
3
4
5
6
/**
 * Thread state for a runnable thread.  A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */

Java 线程的RUNNABLE状态其实包括了操作系统线程的readyrunning两个状态

现代操作系统架构通常都是用所谓的时间分片方式进行抢占式轮转调度。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

📊 BLOCKED(阻塞状态)

线程在试图获取一个锁以进入同步块/方法时,如果锁被其他线程持有,线程将进入阻塞状态,直到它获取到锁。处于 BLOCKED 状态的线程正等待的释放以进入同步区

📊 WAITING(等待状态)

等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒

调用下面这 3 个方法会使线程进入等待状态:

📌 TIMED_WAITING(超时等待)

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

📊 TERMINATE(终止状态)

当线程的 run() 方法执行完毕后,或者因为一个未捕获的异常终止了执行,线程进入终止状态。一旦线程终止,它的生命周期结束,不能再被重新启动,此时线程已执行完毕