# Java并发编程面试题

#

大家好,我是小宇宙。

Java 并发编程是面试里的重头戏,尤其是中高级岗位,这块几乎是必考的。很多人平时工作里用的多线程场景不多,或者用了但没深究原理,到了面试就容易被问住。这块知识点比较分散,从底层的内存模型到上层的线程池配置,跨度很大,但只要理清楚主线,其实是很有逻辑的。

这篇文章整理了 Java 并发面试中最常被问到的知识点,涵盖多线程基础、并发安全机制、锁原理、线程池,以及一些常见的并发场景题。内容偏向原理理解,不是让你背 API,而是帮你搞清楚"这东西为什么这么设计"。

有几块是面试里的高频区域,建议重点投入:

synchronized 和 ReentrantLock:两者的区别、锁升级的过程(偏向锁 → 轻量级锁 → 重量级锁)、以及 AQS 是什么、怎么实现的,这条线是并发面试的核心主线。

volatile 和 CAS:volatile 解决了什么问题、为什么不能保证原子性、CAS 的原理和 ABA 问题,这些问题入门好答,但深挖起来很多人答不全。

ThreadLocal:用途、底层 ThreadLocalMap 的实现、为什么会内存泄漏、怎么避免,这个在面试里出现频率越来越高。

线程池:核心参数的含义、任务提交后的执行流程、几种拒绝策略的区别、以及常见的线程池类型,这块是工程实践和面试的双重重点。

死锁:死锁的四个必要条件、怎么排查、怎么避免,这是个老题,但每次还是会考。

如果你是第一次系统准备这块,建议先搞清楚 JMM 和 synchronized 的原理,再去看 AQS 和线程池,这样整体脉络会更清晰,后面的内容理解起来也会快很多。

# 多线程

#

# Java 的内存模型(JMM)介绍一下

#

JMM 是专门解决多线程并发问题的一套规则。简单说,就是规定了多线程环境下,线程怎么访问共享变量才能不出错,核心是处理可见性、原子性、有序性这三个问题。

先想个场景:两个线程同时操作一个变量,比如线程 A 改了变量的值,线程 B 能不能立刻看到?

如果线程 B 看到的还是旧值,这就是可见性问题。为啥会这样?因为现在 CPU 都有缓存,线程操作变量时,会先把主内存里的变量读到自己的工作内存(比如 CPU 缓存)里,改完可能没及时写回主内存,另一个线程读的还是主内存的旧值。JMM 里像 volatile 关键字就专门解决这个,用了 volatile 的变量,改完会立刻刷回主内存,同时让其他线程的缓存失效,必须重新从主内存读,这样就保证了可见性。

再说说原子性,比如 i++,看着简单,其实是 “读 i、加 1、写回 i” 三步。如果两个线程同时做,可能线程 A 刚读完 i=10,线程 B 就把 i 改成 11,线程 A 再加 1 写回去还是 11,结果就错了。JMM 里用 synchronized 或者 Lock 锁就能保证原子性,加了锁之后,同一时间只有一个线程能执行这三步,中间不会被打断。

还有有序性,就是代码执行顺序可能和你写的不一样。编译器或者 CPU 为了提速,会在不影响单线程结果的情况下调整指令顺序。比如你先初始化对象 A,再把 A 的引用给变量 a,可能被重排成先给引用再初始化。单线程没问题,但多线程下,另一个线程拿到 a 的引用时,A 可能还没初始化好,一调用就报错。这时候 volatile 或者 synchronized 就能通过内存屏障阻止这种重排序,保证顺序正确。

所以 JMM 的核心思路是:定义主内存(大家共享的内存)和工作内存(每个线程自己的缓存),规定变量必须从主内存加载到工作内存才能操作,改完再写回主内存。

然后通过 volatile、synchronized 这些关键字,控制加载、写回的时机,以及禁止不合理的指令重排,最终保证多线程操作共享变量时能正确交互。理解这些,就能明白为啥写并发代码时要用这些关键字,以及它们背后的原理了。

# java多线程是什么?需要注意什么?

#

Java 多线程是指在一个 Java 程序中同时运行多个线程,这些线程共享程序的内存空间(如全局变量、方法区等),但有各自的栈和程序计数器,能同时执行不同的任务,比如一个线程处理用户输入,另一个线程后台下载文件,提升程序效率。

使用 Java 多线程需要注意以下几点:

首先是线程安全问题。多个线程同时操作共享数据时,可能出现错误。比如两个线程同时给一个变量加 1,原本该加 2,结果可能只加了 1,这是因为线程切换时没做好数据保护。需要用synchronized关键字、Lock锁等方式,保证同一时间只有一个线程操作共享数据。

synchronized
Lock

其次是线程间通信。线程需要协作时,比如一个线程生产数据,另一个线程消费数据,要通过wait()、notify()等方法控制,避免出现一方没准备好,另一方就操作的情况,否则可能导致数据错误或线程无限等待。

wait()
notify()

然后是线程的创建和销毁成本。频繁创建和销毁线程会消耗系统资源,影响性能。可以用线程池管理线程,提前创建好一定数量的线程,重复使用,减少资源消耗。

# java里面的线程和操作系统的线程一样吗?

#

要分两种情况回答:平台线程(Platform Thread) 和 虚拟线程(Virtual Thread)。

平台线程:这是 Java 长期以来默认的线程实现。JVM 在 Linux 上通过 pthread_create 创建线程,一个 Java 平台线程对应一个操作系统线程,是严格的 1:1 线程模型,线程的调度、上下文切换都由操作系统内核负责,开销较大,一个线程默认栈空间约 1MB,所以不能无限创建。

pthread_create

虚拟线程:Java 21 正式发布(JEP 444),是 JVM 在用户态自行调度的轻量级线程,采用 M:N 模型——大量虚拟线程会被映射到少量载体平台线程(Carrier Thread)上执行。虚拟线程由 JVM 的 ForkJoinPool 调度,创建成本极低(几百字节起步,可动态扩缩),单 JVM 可以轻松跑上百万个虚拟线程,特别适合 IO 密集型的高并发场景。

ForkJoinPool

所以"Java 线程等于操作系统线程"这种说法只适用于 JDK 21 之前或只使用平台线程的场景;用了虚拟线程后,Java 的并发模型已经很接近 Go 协程的 M:N 调度了。

image-20240725230425385

# 使用多线程要注意哪些问题?

#

要保证多线程的程序是安全,不要出现数据竞争造成的数据混乱的问题。

Java的线程安全在三个方面体现:

原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和synchronized关键字来确保原子性;

可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

# 保证数据的一致性有哪些方案呢?

#

事务管理:使用数据库事务来确保一组数据库操作要么全部成功提交,要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)属性,数据库事务可以保证数据的一致性。

锁机制:使用锁来实现对共享资源的互斥访问。在 Java 中,可以使用 synchronized 关键字、ReentrantLock 或其他锁机制来控制并发访问,从而避免并发操作导致数据不一致。

版本控制:通过乐观锁的方式,在更新数据时记录数据的版本信息,从而避免同时对同一数据进行修改,进而保证数据的一致性。

# 线程的创建方式有哪些?

#

1.继承Thread类

1.继承Thread类

这是最直接的一种方式,用户自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
}
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

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

采用继承Thread类方式

优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread ()方法,直接使用this,即可获得当前线程

缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类

2.实现Runnable接口

2.实现Runnable接口

如果一个类已经继承了其他类,就不能再继承Thread类,此时可以实现java.lang.Runnable接口。实现Runnable接口需要重写run()方法,然后将此Runnable对象作为参数传递给Thread类的构造器,创建Thread对象后调用其start()方法启动线程。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

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

采用实现Runnable接口方式:

优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

实现Callable接口与FutureTask

实现Callable接口与FutureTask

java.util.concurrent.Callable接口类似于Runnable,但Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口。

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();
    }
}
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();
    }
}

采用实现Callable接口方式:

缺点:编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。

优点:线程类只实现 Callable 接口,仍然可以继承其他类;且 call() 方法可以有返回值,也可以抛出异常,配合 FutureTask 能方便地获取异步执行结果。

call()
FutureTask

使用线程池(Executor框架)

使用线程池(Executor框架)

从Java 5开始引入的java.util.concurrent.ExecutorService和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过Executors类的静态方法创建不同类型的线程池。

class Task implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

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

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

采用线程池方式:

缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂。

优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并且,线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。通过合理配置线程池大小,可以最大化CPU利用率和系统吞吐量。

# 怎么启动线程 ?

#

启动线程的通过Thread类的start()。

//创建两个线程, 用start启动线程
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();
//创建两个线程, 用start启动线程
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();

# 如何停止一个线程的运行?

#

主要有这些方法:

异常法停止:线程调用 interrupt() 方法后,在线程的 run 方法中通过 Thread.currentThread().isInterrupted() 判断当前线程的中断状态(注意不要用静态方法 Thread.interrupted(),它会清除中断标志),如果是中断状态则抛出异常,达到中断线程的效果。

interrupt()
Thread.currentThread().isInterrupted()
Thread.interrupted()

在沉睡中停止:先将线程sleep,然后调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。会抛出中断异常,达到停止线程的效果

stop()暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,该方法会有不好的后果:强制让线程停止有可能使一些请理性的工作得不到完成。

使用return停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,能达到停止线程的效果。

# 调用 interrupt 是如何让线程抛出异常的?

#

每个线程都一个与之关联的布尔属性来表示其中断状态,中断状态的初始值为false,当一个线程被其它线程调用Thread.interrupt()方法中断时,会根据实际情况做出响应。

Thread.interrupt()

如果该线程正在执行低级别的可中断方法(如Thread.sleep()、Thread.join()或Object.wait()),则会解除阻塞并抛出InterruptedException异常。

Thread.sleep()
Thread.join()
Object.wait()
InterruptedException

否则Thread.interrupt()仅设置线程的中断状态,在该被中断的线程中稍后可通过轮询中断状态来决定是否要停止当前正在执行的任务。

Thread.interrupt()

# Java线程的状态有哪些?

#

img

源自《Java并发编程艺术》 java.lang.Thread.State枚举类中定义了六种线程的状态,可以调用线程Thread中的getState()方法获取当前线程的状态。

线程状态 解释 NEW 尚未启动的线程状态,即线程创建,还未调用start方法 RUNNABLE 就绪状态(调用start,等待调度)+正在运行 BLOCKED 等待监视器锁时,陷入阻塞状态 WAITING 等待状态的线程正在等待另一线程执行特定的操作(如notify) TIMED_WAITING 具有指定等待时间的等待状态 TERMINATED 线程完成执行,终止状态

线程状态 解释

线程状态

解释

NEW 尚未启动的线程状态,即线程创建,还未调用start方法

NEW

尚未启动的线程状态,即线程创建,还未调用start方法

RUNNABLE 就绪状态(调用start,等待调度)+正在运行

RUNNABLE

就绪状态(调用start,等待调度)+正在运行

BLOCKED 等待监视器锁时,陷入阻塞状态

BLOCKED

等待监视器锁时,陷入阻塞状态

WAITING 等待状态的线程正在等待另一线程执行特定的操作(如notify)

WAITING

等待状态的线程正在等待另一线程执行特定的操作(如notify)

TIMED_WAITING 具有指定等待时间的等待状态

TIMED_WAITING

具有指定等待时间的等待状态

TERMINATED 线程完成执行,终止状态

TERMINATED

线程完成执行,终止状态

# sleep 和 wait的区别是什么?

#

对比例表:

特性 sleep() wait() 所属类 Thread 类(静态方法) Object 类(实例方法) 锁释放 ❌ ✅ 使用前提 任意位置调用 必须在同步块内(持有锁) 唤醒机制 超时自动恢复 需 notify()/notifyAll() 或超时 设计用途 暂停线程执行,不涉及锁协作 线程间协调,释放锁让其他线程工作

特性 sleep() wait()

特性

sleep()

sleep()

wait()

wait()

所属类 Thread 类(静态方法) Object 类(实例方法)

所属类

Thread 类(静态方法)

Thread

Object 类(实例方法)

Object

锁释放 ❌ ✅

锁释放

使用前提 任意位置调用 必须在同步块内(持有锁)

使用前提

任意位置调用

必须在同步块内(持有锁)

唤醒机制 超时自动恢复 需 notify()/notifyAll() 或超时

唤醒机制

超时自动恢复

需 notify()/notifyAll() 或超时

notify()
notifyAll()

设计用途 暂停线程执行,不涉及锁协作 线程间协调,释放锁让其他线程工作

设计用途

暂停线程执行,不涉及锁协作

线程间协调,释放锁让其他线程工作

所属分类的不同:sleep 是 Thread 类的静态方法,可以在任何地方直接通过 Thread.sleep() 调用,无需依赖对象实例。wait 是 Object 类的实例方法,这意味着必须通过对象实例来调用。

Thread
Thread.sleep()
Object

锁释放的情况:Thread.sleep() 在调用时,线程会暂停执行指定的时间,但不会释放持有的对象锁。也就是说,在 sleep 期间,其他线程无法获得该线程持有的锁。Object.wait():调用该方法时,线程会释放持有的对象锁,进入等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法唤醒它

Thread.sleep()
sleep
Object.wait()
notify()
notifyAll()

使用条件:sleep 可在任意位置调用,无需事先获取锁。 wait 必须在同步块或同步方法内调用(即线程需持有该对象的锁),否则抛出 IllegalMonitorStateException。

IllegalMonitorStateException

唤醒机制:sleep 休眠时间结束后,线程 自动恢复 到就绪状态,等待CPU调度。wait 需要其他线程调用相同对象的 notify() 或 notifyAll() 方法才能被唤醒。notify() 会唤醒一个在该对象上等待的线程(具体策略由 JVM 实现决定,HotSpot 实际按 FIFO 顺序唤醒),而 notifyAll() 会唤醒所有在该对象上等待的线程。

notify()
notifyAll()
notify()
notifyAll()

# sleep会释放cpu吗?

#

是的,调用 Thread.sleep() 时,线程会释放 CPU,但不会释放持有的锁。

Thread.sleep()

当线程调用 sleep() 后,会主动让出 CPU 时间片,进入 TIMED_WAITING 状态。此时操作系统会触发调度,将 CPU 分配给其他处于就绪状态的线程。这样其他线程(无论是需要同一锁的线程还是不相关线程)便有机会执行。

sleep()
TIMED_WAITING

sleep() 不会释放线程已持有的任何锁(如 synchronized 同步代码块或方法中获取的锁)。因此,如果有其他线程试图获取同一把锁,它们仍会被阻塞,直到原线程退出同步代码块。

sleep()
synchronized

# blocked和waiting有啥区别

#

区别如下:

触发条件:线程进入BLOCKED状态通常是因为试图获取一个对象的锁(monitor lock),但该锁已经被另一个线程持有。这通常发生在尝试进入synchronized块或方法时,如果锁已被占用,则线程将被阻塞直到锁可用。线程进入WAITING状态是因为它正在等待另一个线程执行某些操作,例如调用Object.wait()方法、Thread.join()方法或LockSupport.park()方法。在这种状态下,线程将不会消耗CPU资源,并且不会参与锁的竞争。

img

唤醒机制:当一个线程被阻塞等待锁时,一旦锁被释放,线程将有机会重新尝试获取锁。如果锁此时未被其他线程获取,那么线程可以从BLOCKED状态变为RUNNABLE状态。线程在WAITING状态中需要被显式唤醒。例如,如果线程调用了Object.wait(),那么它必须等待另一个线程调用同一对象上的Object.notify()或Object.notifyAll()方法才能被唤醒。

所以,BLOCKED和WAITING两个状态最大的区别有两个:

BLOCKED 是锁竞争失败后被动触发的状态,WAITING 是人为主动触发的状态

BLOCKED 的唤醒是自动触发的,而 WAITING 状态必须要通过特定的方法来主动唤醒

# wait 状态下的线程如何恢复到 RUNNABLE 状态?

#

线程从 等待(WAITING) 状态恢复到 RUNNABLE 状态的核心机制是 通过外部事件触发或资源可用性变化,比如等待的线程被其他线程对象唤醒,notify() 和 notifyAll()。(注意:Java Thread.State 枚举中没有 RUNNING 状态,只有 RUNNABLE,包含 OS 层面的 ready 和 running 两个子状态。)

等待(WAITING)
RUNNABLE
notify()
notifyAll()
Thread.State
synchronized (lock) {
    // 线程进入等待状态, 释放锁
    lock.wait(); 
}

// 其他线程调用以下代码唤醒等待线程
synchronized (lock) {
    lock.notify();      // 唤醒单个线程
    // lock.notifyAll(); // 唤醒所有等待线程
}
synchronized (lock) {
    // 线程进入等待状态, 释放锁
    lock.wait(); 
}

// 其他线程调用以下代码唤醒等待线程
synchronized (lock) {
    lock.notify();      // 唤醒单个线程
    // lock.notifyAll(); // 唤醒所有等待线程
}

# notify 和 notifyAll 的区别?

#

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。

区别在于:

notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断

notifyAll:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

# notify 选择哪个线程?

#

notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。

image-20240725230457096

JVM 有很多实现,比较流行的就是 HotSpot,HotSpot 对 notify() 的实现并不是我们以为的随机唤醒,而是"先进先出"的顺序唤醒。

notify()

# 不同的线程之间如何通信?

#

共享变量是最基本的线程间通信方式。多个线程可以访问和修改同一个共享变量,从而实现信息的传递。为了保证线程安全,通常需要使用 synchronized 关键字或 volatile 关键字。

synchronized
volatile
class SharedVariableExample {
    // 使用 volatile 关键字保证变量的可见性
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改共享变量
            flag = true;
            System.out.println("Producer: Flag is set to true.");
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while (!flag) {
                // 等待共享变量被修改
            }
            System.out.println("Consumer: Flag is now true.");
        });

        producer.start();
        consumer.start();
    }
}
class SharedVariableExample {
    // 使用 volatile 关键字保证变量的可见性
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改共享变量
            flag = true;
            System.out.println("Producer: Flag is set to true.");
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while (!flag) {
                // 等待共享变量被修改
            }
            System.out.println("Consumer: Flag is now true.");
        });

        producer.start();
        consumer.start();
    }
}

代码解释

volatile 关键字确保了 flag 变量在多个线程之间的可见性,即一个线程修改了 flag 的值,其他线程能立即看到。

volatile
flag
flag

生产者线程在睡眠 2 秒后将 flag 设置为 true,消费者线程在 flag 为 false 时一直等待,直到 flag 变为 true 才继续执行。

flag
true
flag
false
flag
true

Object 类中的 wait()、notify() 和 notifyAll() 方法可以用于线程间的协作。wait() 方法使当前线程进入等待状态,notify() 方法唤醒在此对象监视器上等待的单个线程,notifyAll() 方法唤醒在此对象监视器上等待的所有线程。

Object
wait()
notify()
notifyAll()
wait()
notify()
notifyAll()
class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Producer: Producing...");
                    Thread.sleep(2000);
                    System.out.println("Producer: Production finished. Notifying consumer.");
                    // 唤醒等待的线程
                    lock.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Consumer: Waiting for production to finish.");
                    // 进入等待状态
                    lock.wait();
                    System.out.println("Consumer: Production finished. Consuming...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        consumer.start();
        producer.start();
    }
}
class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Producer: Producing...");
                    Thread.sleep(2000);
                    System.out.println("Producer: Production finished. Notifying consumer.");
                    // 唤醒等待的线程
                    lock.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Consumer: Waiting for production to finish.");
                    // 进入等待状态
                    lock.wait();
                    System.out.println("Consumer: Production finished. Consuming...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

lock 是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行相应的操作。

lock

消费者线程调用 lock.wait() 方法进入等待状态,释放锁;生产者线程执行完生产任务后调用 lock.notify() 方法唤醒等待的消费者线程。

lock.wait()
lock.notify()

java.util.concurrent.locks 包中的 Lock 和 Condition 接口提供了比 synchronized 更灵活的线程间通信方式。Condition 接口的 await() 方法类似于 wait() 方法,signal() 方法类似于 notify() 方法,signalAll() 方法类似于 notifyAll() 方法。

java.util.concurrent.locks
Lock
Condition
synchronized
Condition
await()
wait()
signal()
notify()
signalAll()
notifyAll()
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockConditionExample {
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Producer: Producing...");
                Thread.sleep(2000);
                System.out.println("Producer: Production finished. Notifying consumer.");
                // 唤醒等待的线程
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                // 进入等待状态
                condition.await();
                System.out.println("Consumer: Production finished. Consuming...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        consumer.start();
        producer.start();
    }
}
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockConditionExample {
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Producer: Producing...");
                Thread.sleep(2000);
                System.out.println("Producer: Production finished. Notifying consumer.");
                // 唤醒等待的线程
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                // 进入等待状态
                condition.await();
                System.out.println("Consumer: Production finished. Consuming...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

ReentrantLock 是 Lock 接口的一个实现类,condition 是通过 lock.newCondition() 方法创建的。

ReentrantLock
Lock
condition
lock.newCondition()

消费者线程调用 condition.await() 方法进入等待状态,生产者线程执行完生产任务后调用 condition.signal() 方法唤醒等待的消费者线程。

condition.await()
condition.signal()

java.util.concurrent 包中的 BlockingQueue 接口提供了线程安全的队列操作,当队列满时,插入元素的线程会被阻塞;当队列为空时,获取元素的线程会被阻塞。

java.util.concurrent
BlockingQueue
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class BlockingQueueExample {
    private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                System.out.println("Producer: Producing...");
                queue.put(1);
                System.out.println("Producer: Production finished.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                int item = queue.take();
                System.out.println("Consumer: Consumed item: " + item);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumer.start();
        producer.start();
    }
}
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class BlockingQueueExample {
    private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                System.out.println("Producer: Producing...");
                queue.put(1);
                System.out.println("Producer: Production finished.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                int item = queue.take();
                System.out.println("Consumer: Consumed item: " + item);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

LinkedBlockingQueue 是 BlockingQueue 接口的一个实现类,容量为 1。

LinkedBlockingQueue
BlockingQueue

生产者线程调用 queue.put(1) 方法将元素插入队列,如果队列已满,线程会被阻塞;消费者线程调用 queue.take() 方法从队列中取出元素,如果队列为空,线程会被阻塞。

queue.put(1)
queue.take()

# 线程间通信方式有哪些?

#

1、Object 类的 wait()、notify() 和 notifyAll() 方法。这是 Java 中最基础的线程间通信方式,基于对象的监视器(锁)机制。

wait():使当前线程进入等待状态,直到其他线程调用该对象的 notify() 或 notifyAll() 方法。

wait()
notify()
notifyAll()

notify():唤醒在此对象监视器上等待的单个线程。

notify()

notifyAll():唤醒在此对象监视器上等待的所有线程。

notifyAll()
class SharedObject {
    public synchronized void consumerMethod() throws InterruptedException {
        while (/* 条件不满足 */) {
            wait();
        }
        // 执行相应操作
    }

    public synchronized void producerMethod() {
        // 执行相应操作
        notify(); // 或者 notifyAll()
    }
}
class SharedObject {
    public synchronized void consumerMethod() throws InterruptedException {
        while (/* 条件不满足 */) {
            wait();
        }
        // 执行相应操作
    }

    public synchronized void producerMethod() {
        // 执行相应操作
        notify(); // 或者 notifyAll()
    }
}

2、Lock 和 Condition 接口。Lock 接口提供了比 synchronized 更灵活的锁机制,Condition 接口则配合 Lock 实现线程间的等待 / 通知机制。

Lock
Condition
Lock
synchronized
Condition
Lock

await():使当前线程进入等待状态,直到被其他线程唤醒。

await()

signal():唤醒一个等待在该 Condition 上的线程。

signal()
Condition

signalAll():唤醒所有等待在该 Condition 上的线程。

signalAll()
Condition
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void consumer() throws InterruptedException {
        lock.lock();
        try {
            while (/* 条件不满足 */) {
                condition.await();
            }
            // 执行相应操作
        } finally {
            lock.unlock();
        }
    }

    public void producer() {
        lock.lock();
        try {
            // 执行相应操作
            condition.signal(); // 或者 signalAll()
        } finally {
            lock.unlock();
        }
    }
}
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void consumer() throws InterruptedException {
        lock.lock();
        try {
            while (/* 条件不满足 */) {
                condition.await();
            }
            // 执行相应操作
        } finally {
            lock.unlock();
        }
    }

    public void producer() {
        lock.lock();
        try {
            // 执行相应操作
            condition.signal(); // 或者 signalAll()
        } finally {
            lock.unlock();
        }
    }
}

3、volatile 关键字。volatile 关键字用于保证变量的可见性,即当一个变量被声明为 volatile 时,它会保证对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值。

volatile
volatile
volatile
class VolatileExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true;
    }

    public void reader() {
        while (!flag) {
            // 等待
        }
        // 执行相应操作
    }
}
class VolatileExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true;
    }

    public void reader() {
        while (!flag) {
            // 等待
        }
        // 执行相应操作
    }
}

4、CountDownLatch。CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。

CountDownLatch

CountDownLatch(int count):构造函数,指定需要等待的线程数量。

CountDownLatch(int count)

countDown():减少计数器的值。

countDown()

await():使当前线程等待,直到计数器的值为 0。

await()
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
        System.out.println("所有线程任务完成");
    }
}
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
        System.out.println("所有线程任务完成");
    }
}

5、CyclicBarrier。CyclicBarrier 是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达某个公共屏障点。

CyclicBarrier

CyclicBarrier(int parties, Runnable barrierAction):构造函数,指定参与的线程数量和所有线程到达屏障点后要执行的操作。

CyclicBarrier(int parties, Runnable barrierAction)

await():使当前线程等待,直到所有线程都到达屏障点。

await()
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程都到达屏障点");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    barrier.await();
                    // 继续执行后续任务
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程都到达屏障点");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    barrier.await();
                    // 继续执行后续任务
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

6、Semaphore。Semaphore 是一个计数信号量,它可以控制同时访问特定资源的线程数量。

Semaphore

Semaphore(int permits):构造函数,指定信号量的初始许可数量。

Semaphore(int permits)

acquire():获取一个许可,如果没有可用许可则阻塞。

acquire()

release():释放一个许可。

release()
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permitCount = 2;
        Semaphore semaphore = new Semaphore(permitCount);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获得许可");
                    // 执行任务
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permitCount = 2;
        Semaphore semaphore = new Semaphore(permitCount);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获得许可");
                    // 执行任务
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}

# 如何停止一个线程?

#

在 Java 中,停止线程的正确方式是 通过协作式的逻辑控制线程终止,而非强制暴力终止(如已废弃的 Thread.stop())。以下是实现安全停止线程的多种方法:

Thread.stop()

第一种方式:通过共享标志位主动终止。定义一个 可见的 状态变量,由主线程控制其值,工作线程循环检测该变量以决定是否退出。

public class SafeStopWithFlag implements Runnable {
    // 使用 volatile 保证可见性
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            try {
                // 处理任务逻辑
                System.out.println("Thread is running...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 捕获中断异常后设置 running=false
                running = false;
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated safely.");
    }

    // 停止线程的方法(由外部调用)
    public void stop() {
        running = false;
    }
}
public class SafeStopWithFlag implements Runnable {
    // 使用 volatile 保证可见性
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            try {
                // 处理任务逻辑
                System.out.println("Thread is running...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 捕获中断异常后设置 running=false
                running = false;
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated safely.");
    }

    // 停止线程的方法(由外部调用)
    public void stop() {
        running = false;
    }
}

调用方式:

SafeStopWithFlag task = new SafeStopWithFlag();
Thread thread = new Thread(task);
thread.start();
// 某个时刻调用停止
Thread.sleep(3000);
task.stop();
SafeStopWithFlag task = new SafeStopWithFlag();
Thread thread = new Thread(task);
thread.start();
// 某个时刻调用停止
Thread.sleep(3000);
task.stop();

第二种方式使用线程中断机制。通过 Thread.interrupt() 触发线程中断状态,结合中断检测逻辑实现安全停止。

Thread.interrupt()
public class InterruptExample implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println("Working...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 当阻塞时被中断, 抛出异常并清除中断状态
                System.out.println("Interrupted during sleep!");
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated by interrupt.");
    }
}
public class InterruptExample implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println("Working...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 当阻塞时被中断, 抛出异常并清除中断状态
                System.out.println("Interrupted during sleep!");
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated by interrupt.");
    }
}

调用方式:

Thread thread = new Thread(new InterruptExample());
thread.start();
// 中断线程
Thread.sleep(3000);
thread.interrupt();
Thread thread = new Thread(new InterruptExample());
thread.start();
// 中断线程
Thread.sleep(3000);
thread.interrupt();

interrupt() 不会立刻终止线程,只是设置中断标志位。

interrupt()

线程需手动检查中断状态(isInterrupted())或触发可中断操作(如sleep(),wait(),join())响应中断。

isInterrupted()
sleep()
wait()
join()

阻塞操作中收到中断请求时,会抛出 InterruptedException 并清除中断状态。

InterruptedException

第三种方式通过 Future 取消任务。使用线程池提交任务,并通过 Future.cancel() 停止线程,依赖中断机制。

Future
Future.cancel()
public class FutureCancelDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<?> future = executor.submit(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Task running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Task interrupted.");
                    Thread.currentThread().interrupt();
                }
            }
        });

        try {
            Thread.sleep(3000);
            future.cancel(true); // true表示尝试中断任务线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}
public class FutureCancelDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<?> future = executor.submit(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Task running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Task interrupted.");
                    Thread.currentThread().interrupt();
                }
            }
        });

        try {
            Thread.sleep(3000);
            future.cancel(true); // true表示尝试中断任务线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

第四种方式处理不可中断的阻塞操作。某些 I/O 或同步操作(如 Socket.accept()、Lock.lock())无法通过中断直接响应。此时需结合资源关闭操作。比如,关闭 Socket 释放阻塞。

Socket.accept()
Lock.lock()
public class SocketHandler implements Runnable {
    private ServerSocket serverSocket;

    public SocketHandler(ServerSocket serverSocket) {
        this.serverSocket = serverSocket;
    }

    @Override
    public void run() {
        try {
            // serverSocket.accept()阻塞时无法响应中断
            while (!Thread.currentThread().isInterrupted()) {
                Socket socket = serverSocket.accept();
                // 处理连接...
            }
        } catch (IOException e) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Thread stopped by interrupt.");
            }
        }
    }

    // 特殊关闭方法(销毁资源)
    public void stop() {
        try {
            serverSocket.close(); // 关闭资源使accept()抛出异常
        } catch (IOException e) {
            System.out.println("Error closing socket: " + e);
        }
    }
}
public class SocketHandler implements Runnable {
    private ServerSocket serverSocket;

    public SocketHandler(ServerSocket serverSocket) {
        this.serverSocket = serverSocket;
    }

    @Override
    public void run() {
        try {
            // serverSocket.accept()阻塞时无法响应中断
            while (!Thread.currentThread().isInterrupted()) {
                Socket socket = serverSocket.accept();
                // 处理连接...
            }
        } catch (IOException e) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Thread stopped by interrupt.");
            }
        }
    }

    // 特殊关闭方法(销毁资源)
    public void stop() {
        try {
            serverSocket.close(); // 关闭资源使accept()抛出异常
        } catch (IOException e) {
            System.out.println("Error closing socket: " + e);
        }
    }
}

调用方式:调用 stop() 方法关闭资源以解除阻塞。

stop()

线程停止的正确实践,如下表格:

方法 适用场景 注意事项 循环检测标志位 简单无阻塞的逻辑 确保标志位使用 volatile 或通过锁保证可见性 中断机制 可中断的阻塞操作 正确处理 InterruptedException 并恢复中断标志 Future.cancel() 线程池管理任务 需要线程池任务支持中断处理机制 资源关闭 不可中断的阻塞操作(如Sockets) 显式关闭资源触发异常,结合中断状态判断回滚

方法 适用场景 注意事项

方法

适用场景

注意事项

循环检测标志位 简单无阻塞的逻辑 确保标志位使用 volatile 或通过锁保证可见性

循环检测标志位

简单无阻塞的逻辑

确保标志位使用 volatile 或通过锁保证可见性

volatile

中断机制 可中断的阻塞操作 正确处理 InterruptedException 并恢复中断标志

中断机制

可中断的阻塞操作

正确处理 InterruptedException 并恢复中断标志

InterruptedException

Future.cancel() 线程池管理任务 需要线程池任务支持中断处理机制

Future.cancel()

线程池管理任务

需要线程池任务支持中断处理机制

资源关闭 不可中断的阻塞操作(如Sockets) 显式关闭资源触发异常,结合中断状态判断回滚

资源关闭

不可中断的阻塞操作(如Sockets)

显式关闭资源触发异常,结合中断状态判断回滚

避免使用以下已废弃方法:

Thread.stop():暴力终止,可能导致状态不一致。

Thread.stop()

Thread.suspend()/resume():易导致死锁。

Thread.suspend()
resume()

# Go 的协程和 Java 的线程有啥区别?

#

Go的协程和Java的线程确实有很大区别,我从几个方面来说一下。

首先最核心的区别是它们的调度模型不一样。Java 的平台线程(Platform Thread)是操作系统级别的线程,一个 Java 平台线程对应一个操作系统线程,创建、销毁、调度都由操作系统内核管理。不过从 Java 21 开始,JEP 444 正式引入了虚拟线程(Virtual Thread),这是 JVM 层面的 M:N 轻量级线程,和 Go 协程的思路已经比较接近了。下面的对比主要针对传统平台线程,虚拟线程的部分会在最后补充说明。

img

而Go的协程,也就是goroutine,它是用户态的轻量级线程,是由Go运行时自己调度的,不需要经过操作系统内核。这就导致了很多后续的差异。

img

从资源消耗上来说,差别非常明显。Java创建一个线程的开销是比较大的,默认情况下一个线程的栈空间大概是1MB左右,而且线程的创建和销毁都需要系统调用,开销不小。所以在Java里你不能无限制地创建线程,创建几千个线程系统可能就扛不住了,这也是为什么Java里要用线程池来复用线程。

但Go的协程就轻量多了,一个goroutine初始栈空间只有2KB,而且栈空间是可以动态伸缩的。Go里创建几万甚至几十万个协程都是很正常的事情,我之前做过一个项目,同时跑了十几万个goroutine,系统压力也不大。

调度方式上也不太一样。Java线程的调度是抢占式的,由操作系统的调度器决定什么时候切换线程,线程切换需要保存和恢复上下文,涉及到用户态和内核态的切换,这个开销相对比较大。

Go的协程调度是在用户态完成的,Go运行时有个GMP模型,G就是goroutine,M是操作系统线程,P是逻辑处理器。Go会把多个协程映射到少量的操作系统线程上执行,协程的切换完全在用户态,不需要陷入内核,所以切换成本非常低。而且Go的调度器会在协程发生阻塞的时候,比如IO操作或者channel操作时,自动把这个协程挂起,让其他协程继续执行,这种协作式调度非常高效。

从使用方式上来讲,Go的协程用起来要简单太多了。在Go里创建一个协程只需要在函数调用前加个go关键字就行了,比如go doSomething(),就这么简单,如下代码:

package main

import (
    "fmt"
    "time"
)

func doSomething(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("%s: %d\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // 创建协程就这么简单, 加个go关键字
    go doSomething("协程1")
    go doSomething("协程2")
    go doSomething("协程3")
    
    // 等待协程执行完
    time.Sleep(time.Second)
    fmt.Println("主程序结束")
}
package main

import (
    "fmt"
    "time"
)

func doSomething(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("%s: %d\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // 创建协程就这么简单, 加个go关键字
    go doSomething("协程1")
    go doSomething("协程2")
    go doSomething("协程3")
    
    // 等待协程执行完
    time.Sleep(time.Second)
    fmt.Println("主程序结束")
}

而在Java里创建线程,你要么new一个Thread对象,要么实现Runnable接口,或者用线程池,总之代码量要多一些。Go这种简洁的语法让并发编程的心智负担降低了很多。

public class RunnableExample {
    public static void main(String[] args) {
        // 实现Runnable接口
        Runnable task = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        
        // 创建Thread对象并传入Runnable
        Thread thread1 = new Thread(task, "线程1");
        Thread thread2 = new Thread(task, "线程2");
        Thread thread3 = new Thread(task, "线程3");
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
public class RunnableExample {
    public static void main(String[] args) {
        // 实现Runnable接口
        Runnable task = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        
        // 创建Thread对象并传入Runnable
        Thread thread1 = new Thread(task, "线程1");
        Thread thread2 = new Thread(task, "线程2");
        Thread thread3 = new Thread(task, "线程3");
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

通信机制也有很大差异。Java线程之间通信主要靠共享内存,需要用synchronized、Lock这些同步机制来保证线程安全,还要小心处理死锁、竞态条件这些问题,写起来比较容易出错。Go虽然也支持共享内存和锁,但它更提倡用channel来通信,有句话叫"不要通过共享内存来通信,而要通过通信来共享内存"。用channel的话,代码会更清晰,也更不容易出现并发问题。

性能上来说,因为Go协程的轻量级特性和高效的调度器,在高并发场景下,Go的表现通常会更好。比如说要处理几万个并发请求,Java可能需要用复杂的异步框架或者线程池来优化,而Go直接为每个请求开一个协程就行了,代码简单性能还好。当然这不是说Java不行,Java在企业级应用、生态完善性这些方面还是有很大优势的,只是在并发模型的设计上,Go确实更现代化一些。

从我实际使用的感受来说,如果是写IO密集型的高并发服务,比如API网关、聊天服务器这种,Go的协程模型开发效率高,性能也好。但如果是传统的企业级应用,需要用到大量的中间件和框架,Java的生态会更成熟一些。两者各有优势,选择哪个还是要看具体场景。

最后再补充一下 Java 21 的虚拟线程。JEP 444 正式发布的虚拟线程,让 Java 终于也有了类似协程的 M:N 调度模型——虚拟线程由 JVM 的 ForkJoinPool 在用户态调度,大量虚拟线程会被映射到少量载体平台线程上执行,创建成本极低(几百字节起步,栈可动态扩缩),单 JVM 可以轻松跑上百万个虚拟线程。创建方式也很简洁:

ForkJoinPool
// 方式一:直接启动虚拟线程
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread"));

// 方式二:通过 Executors 创建每任务一个虚拟线程的线程池
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> { /* 任务 */ });
}
// 方式一:直接启动虚拟线程
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread"));

// 方式二:通过 Executors 创建每任务一个虚拟线程的线程池
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> { /* 任务 */ });
}

当虚拟线程遇到 IO 阻塞(如 Socket、文件、数据库调用)时,JVM 会自动把它从载体线程上"摘下",让载体线程去跑其他虚拟线程,完全不会阻塞操作系统线程——这正是 Go 协程高吞吐的核心原理。所以在 Java 21+ 下,Java 和 Go 在高并发 IO 场景下的差距已经大幅缩小。

# 并发安全

#

# juc包下你常用的类?

#

线程池相关:

ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。

ThreadPoolExecutor

Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。

Executors
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor

并发集合类:

ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。JDK 1.7 采用分段锁(Segment)实现;JDK 1.8 起已废弃分段锁,改为基于 CAS + synchronized 锁桶头节点的方式,锁粒度更细,并发度更高,在高并发场景下比传统的 Hashtable 性能更好。

ConcurrentHashMap
CAS + synchronized
Hashtable

CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。

CopyOnWriteArrayList

同步工具类:

CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。

CountDownLatch
countDown

CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch不同的是,CyclicBarrier可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。

CyclicBarrier
CountDownLatch
CyclicBarrier

Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。

Semaphore

原子类:

AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。

AtomicInteger

AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。

AtomicReference

# 怎么保证多线程安全?

#

synchronized关键字:可以使用synchronized关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized关键字锁定对象的监视器(monitor)来实现的。

synchronized
synchronized
public synchronized void someMethod() { /* ... */ }

public void anotherMethod() {
    synchronized (someObject) {
        /* ... */
    }
}
public synchronized void someMethod() { /* ... */ }

public void anotherMethod() {
    synchronized (someObject) {
        /* ... */
    }
}

volatile关键字:volatile关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。

volatile
public volatile int sharedVariable;
public volatile int sharedVariable;

Lock接口和ReentrantLock类:java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制,ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。

java.util.concurrent.locks.Lock
synchronized
ReentrantLock
private final ReentrantLock lock = new ReentrantLock();

public void someMethod() {
    lock.lock();
    try {
        /* ... */
    } finally {
        lock.unlock();
    }
}
private final ReentrantLock lock = new ReentrantLock();

public void someMethod() {
    lock.lock();
    try {
        /* ... */
    } finally {
        lock.unlock();
    }
}

原子类:Java并发库(java.util.concurrent.atomic)提供了原子类,如AtomicInteger、AtomicLong等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。

java.util.concurrent.atomic
AtomicInteger
AtomicLong

示例:

AtomicInteger counter = new AtomicInteger(0);

int newValue = counter.incrementAndGet(