1.1 java面试题:核心理论 → JUC 工具 → 线程池 → 锁机制 → 常见面试题

多线程是 Java 面试的绝对高地,也是划分"能干"和"能用"的分水岭。我会按照 核心理论 → JUC 工具 → 线程池 → 锁机制 → 常见面试题 的链路,用老练的视角给你拆解,每个点都配上代码和血泪教训。


一、线程基础与核心理论(面试必问)

1. 线程生命周期(六种状态)

复制代码
NEW → RUNNABLE → BLOCKED → WAITING → TIMED_WAITING → TERMINATED
  • BLOCKED:等待 synchronized 锁。
  • WAITINGwait()join()LockSupport.park()
  • TIMED_WAITINGsleep(time)wait(time)LockSupport.parkNanos()
    面试官爱追问sleepwait 区别?
  • sleep 是 Thread 静态方法,不释放锁;wait 是 Object 方法,释放锁,必须在 synchronized 块中调用。

2. 创建线程的方式

  • 继承 Thread,重写 run()
  • 实现 Runnable,作为 Thread 构造参数。
  • 实现 Callable,配合 FutureTask 获取返回值。
    老手只推荐线程池 ,而不是直接 new Thread

3. volatile 的两层语义

  • 可见性:写立即刷回主存,读强制从主存拿。
  • 禁止指令重排序 :通过内存屏障实现。
    但不保证原子性 ,比如 i++ 依然线程不安全。

二、锁机制(从 synchronized 到 AQS)

1. synchronized 的升级之路

  • 偏向锁 → 轻量级锁(自旋)→ 重量级锁(OS 互斥量),JVM 自动优化。
  • 锁对象不能是 String、包装类型等常量对象。
  • 底层原理 :对象头 MarkWord 记录锁状态,monitorenter/monitorexit 指令。

2. Lock 接口与 AQS

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 业务
} finally {
    lock.unlock();
}
  • 公平/非公平new ReentrantLock(true) 按等待队列顺序,吞吐量差。

  • 可中断lock.lockInterruptibly() 可响应中断。

  • 条件变量Condition 实现分组唤醒,替代 Object.wait/notify

    java 复制代码
    Condition notEmpty = lock.newCondition();
    notEmpty.await();
    notEmpty.signal();

3. synchronized vs Lock

特性 synchronized Lock
自动释放 必须 finally unlock
可中断 lockInterruptibly
公平锁 可设置
精准唤醒 只能随机 Condition 分组
性能 高版本优化好 复杂场景更灵活

三、JUC 并发工具(用得最多,问得最深)

1. ConcurrentHashMap 原理

  • JDK7:Segment 分段锁,默认 16 个 Segment。
  • JDK8:CAS + synchronized 锁链表头节点,红黑树优化。
  • put 流程:计算 hash → 死循环 CAS 初始化桶 → 桶为空 CAS 写入 → 有数据锁住头节点插入。

2. CopyOnWriteArrayList

  • 写时复制,写操作加 ReentrantLock,复制新数组。
  • 适合读多写极少场景(如白名单),写多时内存复制开销巨大。

3. CountDownLatch / CyclicBarrier / Semaphore

  • CountDownLatch:一次性,一个线程等 N 个线程完成。

    java 复制代码
    CountDownLatch latch = new CountDownLatch(3);
    // 三个线程执行完 latch.countDown();
    latch.await();
  • CyclicBarrier:可重复,N 个线程互相等待到齐后一起执行。

  • Semaphore:限流,控制同时访问的线程数。

4. CompletableFuture(异步编程王者)

java 复制代码
CompletableFuture.supplyAsync(() -> getUser(userId))
    .thenApply(user -> getOrders(user))
    .exceptionally(e -> Collections.emptyList())
    .thenAccept(orders -> process(orders));
  • 线程池指定:第二个参数传自定义线程池,避免都用 ForkJoinPool。
  • 组合thenCombineallOfanyOf

四、线程池(没自己设计过线程池的 Java 开发不是好开发)

1. 核心参数与执行流程

java 复制代码
ThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime,
                   TimeUnit unit, BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory, RejectedExecutionHandler handler)

流程:

  1. 线程数 < core → 创建新线程。
  2. 线程数 ≥ core → 任务入队列。
  3. 队列满,线程数 < max → 创建新线程。
  4. 队列满,线程数 ≥ max → 执行拒绝策略。

2. 拒绝策略

  • AbortPolicy:抛异常(默认)。
  • CallerRunsPolicy:调用者线程执行。
  • DiscardPolicy:静默丢弃。
  • DiscardOldestPolicy:丢弃队列最老任务。

3. 线程池大小计算公式

  • CPU 密集型:CPU核心数 + 1
  • IO 密集型:CPU核心数 * 2CPU核心数 / (1 - 阻塞系数),阻塞系数 0.8~0.9。
  • 实际要压测!

4. 禁止直接用 Executors

  • FixedThreadPoolSingleThreadPool 使用无界队列,可能 OOM。
  • CachedThreadPool 最大线程数为 Integer.MAX_VALUE,可能 OOM。
  • 必须用 ThreadPoolExecutor 构造,并自定义线程工厂(给线程命名)和拒绝策略。

5. 优雅关闭

java 复制代码
pool.shutdown(); // 不再接受新任务,等待已提交任务执行完
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
    pool.shutdownNow(); // 尝试中断
}

五、高频面试题及话术

1. 线程间如何通信?

  • volatilesynchronized 配合 wait/notify
  • JUC 中的 Lock + Condition
  • CountDownLatchCyclicBarrier
  • 更高级:BlockingQueue 生产者消费者。

2. ThreadLocal 原理及内存泄漏?

  • 每个线程维护一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用。
  • static class Entry extends WeakReference<ThreadLocal<?>>
  • 弱引用 key 被 GC 后,value 无法回收造成泄漏,但 set/get/remove 时会清理 key 为 null 的条目。
  • 最佳实践 :用完必须 remove()

3. 死锁的条件和排查?

  • 互斥、占有且等待、不可抢夺、循环等待。
  • 排查:jstack pid 看线程状态,Found one Java-level deadlock。
  • 避免:按顺序加锁,tryLock(timeout) 尝试。

4. 如何保证多个线程的顺序执行?

  • join():在子线程中调用上一个线程的 join。
  • SingleThreadPool
  • CompletableFuture.thenApplyAsync 链式调用。

六、老手的性能调优经验

  • 锁粒度 :缩小同步代码块,使用 ConcurrentHashMap 替代 Hashtable
  • 锁分离 :如 LinkedBlockingQueue 的 put 和 take 使用不同锁。
  • 无锁化 :CAS、AtomicIntegerLongAdder(高并发累加)。
  • 异步化CompletableFuture + 自定义线程池,提高吞吐。
  • 监控 :用 ThreadMXBean 查看死锁,日志记录线程池队列长度和执行时间。

七、面试模板话术

当面试官让你"聊聊多线程",你可以这样串:

"多线程我理解核心是并发安全和执行效率。安全上,我掌握 volatile、synchronized 和 Lock 的区别与升级,AQS 原理能推导;工具方面熟悉 ConcurrentHashMap 的 CAS+synchronized 实现,用 CountDownLatch 做并行协作,CompletableFuture 编排异步任务。线程池一定自己用 ThreadPoolExecutor 构造,根据 IO 或 CPU 密集型计算公式设置大小,并自定义拒绝策略和命名线程工厂。线上曾通过 jstack 排查过死锁,用 ThreadLocal 传参但记得 remove 防止泄漏。这些是我实际生产中的积累。"

这样既有理论基础,又有踩坑和实战,面试官会认为你确实是经验丰富的 Java 老手。还需深入哪个点,随时说。

在分布式和微服务架构里,事务 这两样东西,直接从单机模式跨到了多节点协同,复杂度翻倍。真正老练的 Java 工程师,不会死背概念,而是能把不同场景下选什么方案、怎么落地、掉过什么坑讲清楚。下面我拆成两部分硬核输出。


一、分布式锁

1. 为什么需要分布式锁?

微服务多实例部署,JVM 层的 synchronized / ReentrantLock 只能锁住自己的进程,无法阻止另一个实例的线程同时执行。比如:

  • 订单支付防止重复扣款
  • 库存扣减防止超卖
  • 定时任务多实例下只跑一次

2. 实现方式对比(面试凸显视野)

方案 原理 优点 缺点
Redis(Redisson) SETNX + Lua 脚本,看门狗续期 性能高,API 友好 极端情况下可能不一致(主从切换)
Zookeeper 临时顺序节点 + Watcher 机制 强一致性,自动释放 性能差,锁释放通知有延迟
数据库 SELECT ... FOR UPDATE 或唯一索引 简单 性能极差,锁表风险

生产主流:中高并发选 Redis(Redisson),强一致且并发不高的场景(如配置变更)选 Zookeeper。

3. Redisson 分布式锁的正确逻辑(重点)

java 复制代码
@Autowired
private RedissonClient redissonClient;

public void pay(String orderId) {
    RLock lock = redissonClient.getLock("order:pay:" + orderId);
    try {
        // 尝试加锁,最多等10秒,锁30秒后自动释放(看门狗会续期)
        if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            // 1. 查订单状态
            Order order = orderMapper.selectById(orderId);
            if (order.getStatus() != 0) return; // 已支付
            // 2. 更新订单
            order.setStatus(1);
            orderMapper.updateById(order);
            // 3. 扣库存
            stockService.deduct(order.getSkuId());
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 关键:保证只有持有锁的线程才能解锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

老手的核心逻辑要点

  • 锁粒度:按业务主键(如订单 ID)加锁,不是全局一把大锁。
  • 看门狗机制:Redisson 默认开启,每 10 秒续期 30 秒,即使业务执行超过 30 秒也不会丢锁,只要 JVM 没挂。
  • 不要手写 SETNX + EXPIRE:非原子,容易造成死锁,Redisson 底层用 Lua 保证原子性。
  • 解锁检查持有者:防止锁超时被释放后,删了别的线程的锁。

4. 分布式锁引发的陷阱与进阶

  • 锁超时业务没执行完怎么办?
    看门狗保底;或者设计业务幂等,真的超时了被释放,也能让后续请求因幂等校验不通过而安全返回。
  • 主从切换锁丢了怎么办?
    红锁(RedLock)方案,上多台独立 Redis,大多数节点加锁成功才算获取,但性能开销大,大部分公司用哨兵/Cluster + 幂等兜底就够了。
  • 性能优化 :锁的读写分离------读操作不加锁或使用读写锁 RReadWriteLock

二、分布式事务

1. 经典模型概览

方案 原理 适用场景
XA 二阶段提交 事务管理器协调,prepare + commit 强一致性,性能极差
TCC(Try-Confirm-Cancel) 业务层面预留、确认或取消 资金转账、扣减类,性能较好
Saga 长事务拆成多个本地事务,有补偿操作 流程长、对一致性要求不太严的订单流程
AT 模式(Seata) 自动生成回滚日志,代理数据源 无侵入,类似本地事务编程,生产中较广

2. Seata AT 模式实战(最接近微服务落地)

Seata 的 AT 模式相当于无侵入的分布式事务中间件 ,业务代码只用加一个 @GlobalTransactional

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountFeignClient accountClient;
    @Autowired
    private StorageFeignClient storageClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 创建订单,本地事务
        orderMapper.insert(dto);
        // 2. 远程调用扣减账户余额
        accountClient.debit(dto.getUserId(), dto.getAmount());
        // 3. 远程调用扣减库存
        storageClient.deduct(dto.getSkuId(), dto.getCount());
    }
}

背后逻辑

  1. 一阶段 :Seata 代理各服务数据源,业务 SQL 执行时自动生成回滚日志(undo_log),和业务 SQL 一起提交到本地数据库。
  2. 二阶段提交:全局事务管理器接到所有分支成功通知后,异步删除 undo_log。
  3. 二阶段回滚:任何一个分支失败,TM 通知各 RM 根据 undo_log 反向补偿(生成反向 SQL 执行还原)。

老手的注意点

  • undo_log 表必须在每个业务库中创建,定期清理。
  • AT 模式隔离级别默认为读未提交 ,要用 @GlobalLock + SELECT FOR UPDATE 加强。
  • 性能损耗主要在一阶段事务的等待和二阶段的异步处理,一般比 TCC 慢一点,但开发成本极低。

3. TCC 场景代码示意(给你思路)

如果扣钱、扣库存必须强隔离,用 TCC 自己写准备、提交、回滚接口。

java 复制代码
// 账户服务接口
public interface AccountTccService {
    @Transactional
    void tryDebit(String userId, BigDecimal amount);  // 冻结资金
    void confirm(String userId);                      // 扣减冻结资金
    void cancel(String userId);                       // 解冻资金
}

// 业务调用方
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    orderMapper.insert(dto);
    accountTccService.tryDebit(dto.getUserId(), dto.getAmount());
    storageTccService.tryDeduct(dto.getSkuId(), dto.getCount());
}

每个 try 在本地库预留资源(如把余额从可用转冻结),如果全局失败,cancel 退回资源。

4. 分布式事务的最终一致性替代方案

很多时候,我们并不需要强一致的分布式事务,用本地消息表 + MQ 最终一致性是更可靠的选择:

  • 下单:订单服务创建订单 + 本地消息表(状态=待发送),然后发送 MQ。
  • 消费方:库存服务消费消息扣库存,成功则确认;失败则本地重试或回退。
  • 定时任务:扫描消息表,重发失败消息。

老手会这样权衡:"如果是涉及资金强一致性且并发不高,用 TCC;如果是订单和库存这类,用 Seata AT 或 MQ 最终一致性;如果完全不需要一致,纯补偿就行。"

面试话术模板:

"分布式锁我以 Redis Redisson 为主,看门狗续期防死锁,配合业务幂等解决超时问题,红锁了解但没用过。分布式事务我的原则是能不用就不硬上,大部分订单+库存通过 MQ 保证最终一致;对必须强一致的扣减场景,我用 Seata AT 模式零侵入,或者 TCC 自定义资源预留和确认/取消,核心是保证幂等和补偿可执行。无论哪种,异常重试和监控告警必须配套。"

这样既展现了方案深度,又点出了你的工程权衡能力,而不是生搬概念。

好的,这个问题我们要用老练的 Java 工程师视角来回答:先讲清楚哪些集合线程安全、哪些不安全 ,然后扩展到在整个 Java 技术栈中如何系统性地规避线程不安全,这部分才是面试官最想听的工程思维。


一、Java 集合中的线程安全与不安全

1. 线程安全的集合(自带安全机制)

集合 底层安全机制 特点
Vector 所有方法 synchronized 古老,性能差,已弃用
Hashtable 所有方法 synchronized 同 Vector,不用了
Collections.synchronizedList/Map/Set 包装器,加 synchronized 代码块 锁粒度粗,慎用
CopyOnWriteArrayList / CopyOnWriteArraySet 写时复制 (ReentrantLock + 数组复制) 读全无锁,适合读远多于写
ConcurrentHashMap JDK1.8 CAS + synchronized 锁头节点 分段思想,多线程协同扩容,高并发首选
ConcurrentSkipListMap / Set 跳表 + CAS 高并发且有序
BlockingQueue 实现(ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, DelayQueue) ReentrantLock + Condition 生产者消费者利器
ConcurrentLinkedQueue / Deque CAS 无锁算法 高并发非阻塞队列

2. 线程不安全的集合(单机无控制)

  • ArrayList, LinkedList
  • HashSet, TreeSet
  • HashMap, TreeMap, LinkedHashMap
  • PriorityQueue(非线程安全)
  • 迭代时修改会导致 ConcurrentModificationException(fail-fast)

二、在 Java 中系统性地规避线程不安全

老手从不指望单一手段,而是根据场景组合策略,这里我按从底层到架构的顺序讲。

1. 语言级不可变对象(最彻底的线程安全)

对象创建后状态不可改变,自然线程安全。

java 复制代码
public final class ImmutableUser {
    private final String name;
    private final int age;
    // 构造赋值,无 setter
}
// 集合不可变视图
List<String> list = Collections.unmodifiableList(originalList);
List.of("a", "b", "c"); // Java 9+ 本身不可变

StringIntegerBigDecimal 等不可变类天生安全。

2. 同步包装器(快速补救,粒度粗)

java 复制代码
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

注意 :迭代时仍需手动 synchronized 块,否则会 fail-fast。

3. JUC 并发集合(细粒度锁,主流方案)

  • ConcurrentHashMap 替代 HashMap:CAS + synchronized 仅锁桶,支持并发扩容,读无锁。
  • CopyOnWriteArrayList 替代 ArrayList:写复制整数组,读原数组,适合配置、白名单等读多写极少场景。
  • BlockingQueue 替代手动 wait/notify:有界队列防止 OOM,put/take 阻塞唤醒。
  • ConcurrentLinkedQueue:非阻塞 CAS,吞吐高。

案例 :高并发计数器用 ConcurrentHashMap<String, LongAdder>,避免锁。

4. 显式锁与同步块(灵活控制,复杂逻辑专用)

  • synchronized 代码块 / 方法:适合简单互斥。
  • ReentrantLock + Condition:可中断、超时、公平、分组唤醒。
java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

5. 原子类与 CAS(无锁算法,超高并发)

  • AtomicInteger, AtomicLong, AtomicReference:基于 CAS,自旋更新。
  • LongAdder:高并发累加性能优于 AtomicLong,内部用 Cell 数组分散热点。
java 复制代码
LongAdder counter = new LongAdder();
counter.increment();

适合计数、统计等场景。

6. volatile 保证可见性(不保证原子性)

java 复制代码
volatile boolean flag = true;

一个线程修改 flag,其他线程立即可见。

注意flag++ 仍线程不安全,需 AtomicIntegersynchronized

7. 线程封闭(ThreadLocal、栈封闭)

数据不共享,自然无线程安全问题。

  • ThreadLocal :每个线程一份副本,适合日期格式化、上下文传递。

    java 复制代码
    private static ThreadLocal<SimpleDateFormat> threadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    用后必须 remove() 防止内存泄漏。

  • 栈封闭:方法内局部变量,不被外泄。

8. 线程池与异步编排(管理线程资源,分离任务)

  • ThreadPoolExecutor 显式定义有界队列、拒绝策略,避免无节制创建线程。
  • CompletableFuture 编排异步任务,用 thenApplyAsync 指定线程池,避免共享数据竞争。

9. 设计层面规避:无状态服务、消息传递

  • 无状态 Bean:Spring 单例 Service 不存储可变状态,只依赖局部变量和线程安全参数。
  • 消息队列:服务间通过 MQ 异步传递数据,减少共享内存,各自处理自己的数据库事务,保证最终一致性。

10. 测试与监控

  • IDEA 的 FindBugs/SpotBugs 静态分析,标注 @ThreadSafe / @NotThreadSafe
  • 编写并发测试,使用 CyclicBarrier 同时放行,CountDownLatch 等待结束,检测数据一致性。
  • 线上用 jstack 分析死锁,JMX 监控线程池。

三、面试王者话术

"Java 里线程安全我分几个层面来保证。数据类首选不可变对象,集合选 ConcurrentHashMapCopyOnWriteArrayList,计数器用 LongAdder

复杂同步逻辑用 ReentrantLock 实现可中断和超时控制,配合 Condition。读写多写少用读写锁,需要有序用 ConcurrentSkipListMap

业务中尽量设计无状态 Service,必须共享的状态通过 ThreadLocal 隔离,用完清除。线程池全部自定义,有界队列 + 拒绝策略防止 OOM。

最后,并发 bug 难以重现,我会用静态分析 + 并发测试 + 监控日志来兜底。"

这样回答既展示了集合层面的选择,又有语言机制 → 并发包 → 设计思想 → 测试保障的全链路规避思路,面试官会觉得你完全掌握了 Java 线程安全的精髓。