JAVA 多线程面试题
[toc]
什么是原子性?
原子性指的是一个或者多个操作,要么全部执行,要么都不执行,再执行的过程中,不被其他操作中断或干扰。
在并发过程中,原子性是一个非常重要的概念,如果操作不是原子的,在多线程并发环境下,多个线程可能访问同一个变量,可能会导致数据不一致。
Java 中基本数据类型的赋值和读取是原子的吗?
处理long 和 double (可以通过 volatile 修饰实现原子性)注意:volatile 修饰的 i++,i--操作不具有原子性,volatile 主要是为了解决可见性,其他的基本数据类型的读取和赋值都是原子性的。
int x = 100;//原子
x++;//非原子
int y = x;//非原子
x = x + 1;//非原子上面的代码只有第一行是原子的,其他三个都包含两个以上的操作,首先都是要求读取 x 变量的值,经过计算后再将新的值写入到内存中去
volatile的局限性
- 不保证原子性:
- 对
volatile变量的复合操作(如i++)仍是非原子的,可能被线程切换打断。- 不解决指令重排序:
- 编译器或 CPU 仍可能重排
volatile与非volatile操作的顺序(除非配合内存屏障)。- 不适用于多线程同步:
- 现代多核 CPU 的缓存一致性协议(如 MESI)通常能自动同步内存,
volatile的可见性作用被弱化,而原子性和顺序性成为更关键的问题。
什么是可见性?如何保证可见性?
可见性是指一个线程对共享变量的修改,其他线程可以立刻看到该值。
多个线程可能会访问同一个变量,由于线程本地缓存(如 cpu 缓存)的存在,一个线程对变量的修改,但没有及时刷新到到主内存,其他线程读取的可能是旧的副本值,而不是最新的。这就导致了可见性问题。
如何保证可见性?
volatile 关键字。
- volatile 保证的了对变量的写操作会立刻写入到主内存中去。
- 对变量的读操作会从主内存中读取最新值。
- 保证可见性,但是不保证原子性。
使用 Synchronized 或 lock
Synchronized 关键字不仅保证互斥性(即同一个时刻只有一个线程执行临界区代码)还保证可见性:进入 Synchronized 代码块时,会清空本地缓存,从主内存中读取最新值;退出时,会将变量刷新到主内存中去。
使用 final(初始化时的可见性)
使用 final 字段,java 在构造函数完成后,会确保所有线程都能够看到该字段的正确值。
什么是有序性?如何保证有序性?
有序性指代码运行的顺序符合代码编写时的先后顺序。
但是在实际执行中,出于优化的目的(如提高性能),编译器、CPU、和 java 虚拟机都可能对代码进行“指令重排序”,从而打破原本代码书写的顺序。这就可能导致并发程序中产生预期之外的结果。
如何保证有序性?
volatile 关键字
限制了某些类型的指令重排序
写入 volatile 变量之前的所有操作,在内存语义上都会在写操作之前完成
保证了 写-读操作的有序性。
javavolatile boolean ready = false; int a = 0 //线程 A修改值 a=42; read = true; //volatile 写屏障,强制前面的先执行 //线程 B if (ready) { System.out.println(a);//保证看到 a=42 }
Synchronized / lock
- Synchronized 不仅保证互斥性和可见性,还会禁止同步块内的重排序。
- 进入同步块:读取主内存,禁止重排序
- 退出同步块;刷行变量到主内存,保证前后代码的顺序。
内存屏障(Memory Barrier)或 happens-before 规则(JMM 内存模型)
- Java 内存模型通过 happens-before 关系来定义哪些操作之间是有序的。
- 比如:
- Synchronized 之间的操作是有序的
- volatile 写->读是有序的
- 线程 start() 之前的操作,对新线程是可见且有序的
- 线程 join()之后的操作,一定在被 join 的线程所有操作之后
为什么要使用多线程?
更好的利用多核 cpu(并行计算)
提升程序的响应速度(改善用户体验)
提高程序执行效率(cpu利用率),防止阻塞
单线程程序在 io 时会阻塞,cpu 闲置,多线程可以在一个线程等待时,其他线程继续工作,从而充分利用 cpu。
创建线程的几种方式?
继承 Thread 类重新 run 方法
实现 runable 接口重写run 方法
实现 Callable 接口+FutureTask(有返回值)
javaimport java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<String> { @Override public String call() { return "Result from Callable"; } } public class Main { public static void main(String[] args) throws Exception { FutureTask<String> task = new FutureTask<>(new MyCallable()); new Thread(task).start(); //task.get()方法会阻塞 System.out.println("Callable result: " + task.get()); } }需要注意的是
- callable 是一个可以产生返回值的任务。
- 你可以通过 future 或 futureTask获取这个返回值;
- 如果你不调用 get() 方法,主线程不会阻塞
什么是守护线程与非守护线程?如何创建?
守护线程是指在后台默默提供服务的线程。当所有用户线程(非守护线程)都结束后,jvm 会自动退出,及时守护线程还在运行。常用于垃圾回收、异步清理任务、日志、监控、心跳等后台服务线程
非守护线程(也称之为用户线程)是程序中默认创建的线程,JVM 会一直运行,知道所有非守护线程结束,才会退出。
Thread 有一个属性用户区分时候是守护线程,通过 Thread.setDaemon(true) 来设置(必须在启动之前)
线程的几种状态,如果流转的?
Java 中线程一共有 6 种状态,由java.lang.Thread.State 枚举定义:
| 状态名 | 含义 |
|---|---|
NEW | 新建状态:线程已创建但尚未启动 |
RUNNABLE | 可运行状态:正在运行或等待 CPU 调度 |
BLOCKED | 阻塞状态:等待锁(monitor) |
WAITING | 无限期等待:等待别的线程显式唤醒 |
TIMED_WAITING | 有时间限制的等待:如 sleep()、join(1000) |
TERMINATED | 终止状态:线程执行完毕或异常退出 |
生命周期:
┌──────────┐
│ NEW │
└────┬─────┘
│ start()
▼
┌────────────┐
│ RUNNABLE │
└───┬─┬──────┘
│ │
│ └────────────┐
▼ ▼
┌─────────┐ ┌──────────────┐
│ BLOCKED │ │ WAITING │◄─────────┐
└────┬────┘ └──────────────┘ │
│ ▲ │
│ │ │
▼ │ │
┌──────────────┐ │ ┌──────────────┐
│ TIMED_WAITING│───────┘ │TERMINATED │
└──────────────┘ └──────────────┘线程的优先级有什么用?
它的作用是"影响线程被 CPU 调度的概率"
- 高优先级线程在理论上更有可能被操作系统线程调度器选中。
- 但这只是一个指标,具体行为依赖与操作系统和 JVM 实现。不能依赖它来保证顺序性或者公平性。(优先级不是抢占式控制,不是强制调度!)
java 中每个线程都有一个整数优先级,取值范围是:
| 常量名 | 值 | 说明 |
|---|---|---|
Thread.MIN_PRIORITY | 1 | 最低优先级 |
Thread.NORM_PRIORITY | 5(默认) | 普通优先级 |
Thread.MAX_PRIORITY | 10 | 最高优先级 |
可用通过以下当时设置优先级:
Thread t = new Thread(...);
t.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级线程优先级只是调度建议,不是强制控制,不能依赖它实现功能或控制顺序,但可用于性能调优和资源让渡。
怎么让 3 个线程按顺序执行?编程实现?
使用 join()方法,join()方法的主要作用是让当前线程等待调用 join 的线程执行完成后再继续执行
javapackage com.hongsipeng.juc; /** * @author hongsipeng * @apiNote join 方法测试 * @since 2025/7/30 */ public class JoinExample { public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println("线程 1 开始"); }); // join() 方法可以让一个线程等待另一个线程执行完毕 Thread thread2 = new Thread(() -> { try { thread1.join(); System.out.println("线程 2 开始"); } catch (InterruptedException e) { throw new RuntimeException(e); } }); Thread thread3 = new Thread(() -> { try { thread2.join(); System.out.println("线程 3 开始"); } catch (InterruptedException e) { throw new RuntimeException(e); } }); thread1.start(); thread2.start(); thread3.start(); } }使用单线程池,线程池中只有一个核心线程,提交任务给线程池时,线程任务会被阻塞
javapackage com.hongsipeng.juc; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author hongsipeng * @apiNote 单线程池控制线程按照顺序执行 * @since 2025/7/30 */ public class SingleThreadExecutorExample { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(()->{ System.out.println("线程 1 执行"); }); executorService.execute(()->{ System.out.println("线程 2 执行"); }); executorService.execute(()->{ System.out.println("线程 3 执行"); }); executorService.shutdown(); //如果不加这个,程序不会终止 } }使用 Wait 和 notify 机制
javapackage com.hongsipeng.juc; /** * @author hongsipeng * @apiNote wait notify example * @since 2025/8/3 */ public class WaitNotifyExample { private static int currentThread = 1; public static void main(String[] args) { Object lock = new Object(); new Thread(() -> { synchronized (lock) { while (currentThread != 1) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); currentThread = 2; lock.notifyAll(); } }).start(); new Thread(() -> { synchronized (lock) { while (currentThread != 2) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); currentThread = 3; lock.notifyAll(); } }).start(); new Thread(() -> { synchronized (lock) { while (currentThread != 3) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); currentThread = 4; lock.notifyAll(); } }).start(); } }使用 Condition 实现
javapackage com.hongsipeng.juc; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author hongsipeng * @apiNote 使用 Condition实现线程按照顺序执行 * @since 2025/8/4 */ public class ThreadOrderWithConditionTest { private static final Lock lock = new ReentrantLock(); private static final Condition condition1 = lock.newCondition(); private static final Condition condition2 = lock.newCondition(); private static final Condition condition3 = lock.newCondition(); private static int currentThread = 1; public static void main(String[] args) { new Thread(() -> { try { lock.lock(); while (currentThread != 1) { try { condition1.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); currentThread = 2; condition2.signal(); } finally { lock.unlock(); } }).start(); new Thread(() -> { try { lock.lock(); while (currentThread != 2) { try { condition2.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); currentThread = 3; condition3.signal(); } finally { lock.unlock(); } }).start(); new Thread(() -> { try { lock.lock(); while (currentThread != 3) { try { condition3.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread " + currentThread + " is over"); condition1.signal();//如果需要循环执行 } finally { lock.unlock(); } }).start(); } }相比 wait 和 notify,condition 提供了更精确的线程唤醒机制,可以为不同的等待条件创建不同的 Condition 对象。
注意:必须在 lock 和 unlock 之间调用 condition 的方法,否则会抛出IllegalMonitorStateException 异常
使用 CountDownLatch
javapackage com.hongsipeng.juc; import java.util.concurrent.CountDownLatch; /** * @author hongsipeng * @apiNote CountDownLatch 测试例子,控制线程按照顺序执行 * @since 2025/8/4 */ public class CountDownLatchExample { static CountDownLatch countDownLatch1 = new CountDownLatch(1); static CountDownLatch countDownLatch2 = new CountDownLatch(1); public static void main(String[] args) { new Thread(() -> { System.out.println("线程 1 执行"); countDownLatch1.countDown(); }).start(); new Thread(() -> { try { countDownLatch1.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程 2 执行"); countDownLatch2.countDown(); }).start(); new Thread(() -> { try { countDownLatch2.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程 3 执行"); }).start(); } }使用 CompletableFuture
javapackage com.hongsipeng.juc; import java.util.concurrent.CompletableFuture; /** * @author hongsipeng * @apiNote CompletableFuture 测试类 * @since 2025/8/4 */ public class CompletableFutureExample { public static void main(String[] args) { CompletableFuture.runAsync(()->{ System.out.println("线程 1 执行"); }).thenRun(()->{ System.out.println("线程 2 执行"); }).thenRun(()->{ System.out.println("线程 3 执行"); }).join(); } }CompletableFuture 是 java8 引入的一个强大的异步编程工具,属于 java.util.concurrent包下,它是对 future 的增强版,支持链式调用,组合异步调用,异常处理等。详细介绍请看本文后半部分。
join 方法有什么用?什么原理?编程实现?
join()方法是
如何让一个线程休眠?
启动一个线程是 start 还是 run? 两者有什么区别?
一个线程多次调用 start 会发生什么?
线程 sleep 和 wait 的区别?
Thread.yield方法有什么用?yield 方法和 sleep 方法的区别?编程实现 yield 的例子?
怎么理解 java 中的线程中断?中断和stop 的区别?编程实现一个线程中断的例子?
你怎么理解多线程分组?编程实现一个线程分组的例子
如何理解 wait、notify、notifyAll?为什么这三个方法不是 Thread 类中的,而是 Object 类中的,使用过程中有什么需要注意的吗?
Wait、notify、notifyAll 是 Object 类中的重要方法,用于线程之间的协调通信,它们是 java 多线程编程中实现线程间下作的基础机制。
wait方法:让当前线程进入等待状态,并释放所持有的锁,直到被唤醒
- 使当前线程进入等待状态,释放对象的锁
- 必须在同步代码块或同步方法中调用(即必须先获得对象的监视器锁(对象的监视器锁指的是 jvm级别的锁的内置实现))
- 调用后线程会一直等待,直到其他线程调用对象的 notify 或 notifyAll()方法
- 有三个重载版本
- wait() - 无限等待
- wait(long timeOut) - 等待指令毫秒数
- wait(long timeOut, int nanos) - 更精确的等待时间
notify 方法:随机唤醒一个正在等待该对象锁的线程
- 唤醒一个正在等待该对象监视器的线程
- 同样该方法必须在同步代码块或同步方法中调用
- 如果有多个线程在等待,JVM 会任意选择一个线程唤醒,被唤醒的线程不会立刻执行,会重新竞争获取锁。
notifyAll 方法:唤醒所有正在等待该对象锁的进程,让它们竞争锁
- 唤醒所有正在等待该对象监视器的线程
- 这些线程将竞争获取锁对象
- 通过比 notify()更安全,避免某些线程永远等待
为什么这三个方法定义在 Object 类,而不是 Thread类?
这是一个设计上的关键决策,原因如下
锁是基于对象的(Monitor 机制)
- java 的同步机制是基于对象的,每个对象都有一个内置锁(Monitor)
- wait()、notify()和 notifyAll() 是锁的协作机制,必须和 Synchronized 配合使用,如果它们定义在 Thread类,就无法和对象锁直接关联,导致设计混乱
线程可以等待多个不同的对象
一个线程可以同时持有多个对象的锁,并在不同条件下等待:
javasynchronized (lock1) { synchronized (lock2) { lock1.wait(); // 释放 lock1,但仍持有 lock2 } }如果 wait() 是 Thread 的方法,就无法指定等待那个对象的锁。
notify 需要明确目标对象,如果 notify 是 Thread 的方法,就无法精确控制唤醒哪个锁上的线程
更符合面向对象的设计
注意事项
- 必须在同步代码块中使用,否则会抛出 IllegalMonitorStateException
- 通常 Wait() 方法应该放在循环中检查条件(使用 While 而不是 if)防止虚假唤醒
- notify 和 notifyAll 的使用取决于具体使用场景
- 被 notify()或 notifyAll()唤醒的线程在重新获取锁后,会从 wait() 调用之后继续执行,而不是从头开始执行同步块(wait的时候或保存上下文环境)
- 从 java 5 开始,更推进使用 java.util.concurrent 包中的高级同步工具,如 Conditon,BlockingQueue 等)
