并发编程 六

Java多线程核心面试知识点

一、线程协作:join()方法的等待通知机制

1.1 核心原理

join()方法的本质是让当前线程等待目标线程执行完毕后再继续执行 ,其底层基于Java的等待/通知机制实现。当调用thread.join()时,当前线程会进入等待状态,直到目标线程终止(isAlive()返回false),JVM会自动唤醒等待在该线程对象上的所有线程。

1.2 多米诺骨牌式线程执行示例

你的代码完美展示了"链式等待"的效果:每个线程等待前一个线程终止后才执行,最终形成从主线程开始,依次0→1→2→...→9的执行顺序。

java 复制代码
import java.util.concurrent.TimeUnit;

public class JoinDemo {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread(); // 初始前驱是主线程
        
        for (int i = 0; i < 10; i++) {
            // 每个线程持有前一个线程的引用
            Thread thread = new Thread(new DominoTask(previous), String.valueOf(i));
            thread.start();
            previous = thread; // 更新前驱为当前线程
        }
        
        TimeUnit.SECONDS.sleep(5); // 主线程休眠5秒
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }
    
    static class DominoTask implements Runnable {
        private final Thread predecessor;
        
        public DominoTask(Thread predecessor) {
            this.predecessor = predecessor;
        }
        
        @Override
        public void run() {
            try {
                // 等待前驱线程执行完毕
                predecessor.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

1.3 执行结果与分析

复制代码
main terminate.
0 terminate.
1 terminate.
2 terminate.
...
9 terminate.
  • 主线程休眠5秒后先打印终止信息
  • 线程0等待主线程终止后执行
  • 线程1等待线程0终止后执行,以此类推

1.4 面试要点

  • join(long millis):带超时时间的等待,避免无限阻塞
  • 中断处理:join()会抛出InterruptedException,捕获后建议恢复线程中断状态
  • 底层实现:本质是调用了线程对象的wait()方法,线程终止时JVM会调用notifyAll()唤醒等待线程

二、ThreadLocal:线程私有变量的实现原理

2.1 核心概念

ThreadLocal提供了线程级别的变量隔离,每个线程都有自己独立的变量副本,互不干扰。它解决了多线程环境下变量共享的线程安全问题,适用于每个线程需要自己独立实例的场景(如数据库连接、用户会话信息)。

2.2 底层实现原理(面试重点)

ThreadLocal的核心是Thread类中的threadLocals变量,它是一个ThreadLocal.ThreadLocalMap类型的哈希表:

  1. 存储结构 :每个Thread对象维护一个ThreadLocalMap,键是ThreadLocal实例本身,值是线程私有变量
  2. 弱引用设计ThreadLocalMap中的Entry继承自WeakReference<ThreadLocal<?>>,即键是弱引用,值是强引用
  3. get/set流程
    • set(value):获取当前线程的ThreadLocalMap,以当前ThreadLocal为键存储值
    • get():获取当前线程的ThreadLocalMap,以当前ThreadLocal为键查找值
    • remove():从ThreadLocalMap中删除当前ThreadLocal对应的键值对

2.3 内存泄漏问题与解决方案

问题:如果ThreadLocal没有被外部强引用,垃圾回收时会回收键(弱引用),但值仍然被Entry强引用,导致值无法回收,造成内存泄漏。

解决方案

  • 每次使用完ThreadLocal后,显式调用remove()方法删除键值对
  • 尽量将ThreadLocal声明为private static final,延长其生命周期,减少频繁创建销毁

2.4 代码示例

java 复制代码
public class ThreadLocalDemo {
    // 声明为private static final是最佳实践
    private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    int value = threadLocal.get();
                    value += 1;
                    threadLocal.set(value);
                    System.out.println(Thread.currentThread().getName() + ": " + value);
                } finally {
                    threadLocal.remove(); // 必须显式移除,防止内存泄漏
                }
            }, "Thread-" + i).start();
        }
    }
}

2.5 纠正你的笔记错误

ThreadLocal和Session的实现原理不一样

  • ThreadLocal:基于线程隔离,每个线程有自己的变量副本,生命周期与线程一致
  • Session:基于会话隔离,每个用户会话有自己的信息,通常通过Cookie传递Session ID,服务器端维护一个全局的Session Map,生命周期与会话一致
  • 两者只是思想相似("每个X有自己的Y"),但底层实现和应用场景完全不同

三、线程池:Java并发编程的核心

3.1 为什么使用线程池

  • 降低资源消耗:避免频繁创建和销毁线程的开销
  • 提高响应速度:任务到达时无需等待线程创建即可执行
  • 提高线程的可管理性:统一分配、调优和监控线程,避免无限制创建线程导致系统崩溃

3.2 线程池核心参数(ThreadPoolExecutor)

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻线程)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程创建工厂
    RejectedExecutionHandler handler   // 拒绝策略
)

3.3 线程池工作流程

  1. 任务提交时,若核心线程数未满,创建核心线程执行任务
  2. 若核心线程数已满,将任务加入任务队列
  3. 若任务队列已满,创建非核心线程执行任务
  4. 若总线程数达到maximumPoolSize,执行拒绝策略

3.4 常见线程池类型

线程池类型 核心特点 适用场景
newFixedThreadPool(n) 固定线程数,无界队列 执行长期任务,控制并发数
newCachedThreadPool() 无界线程池,自动回收空闲线程 执行大量短期异步任务
newSingleThreadExecutor() 单线程,保证任务顺序执行 需要串行执行任务的场景
newScheduledThreadPool(n) 支持定时和周期性任务执行 定时任务调度

3.5 代码示例:自定义线程池

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 自定义线程池(推荐方式,避免使用Executors创建)
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // 核心线程数
            5, // 最大线程数
            60L, // 空闲线程存活时间
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10), // 有界队列
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() // 默认拒绝策略:抛出异常
        );
        
        try {
            // 提交10个任务
            for (int i = 0; i < 10; i++) {
                final int taskId = i;
                executor.submit(() -> {
                    System.out.println("任务" + taskId + "由线程" + Thread.currentThread().getName() + "执行");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        } finally {
            executor.shutdown(); // 关闭线程池
        }
    }
}

3.6 面试要点

  • 不推荐使用Executors创建线程池,因为它们可能导致OOM(如newCachedThreadPool无界线程数,newFixedThreadPool无界队列)
  • 拒绝策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃最旧任务)
  • 线程池状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED

四、锁的重入:可重入锁的原理与应用

4.1 什么是锁的重入

可重入锁 :当一个线程已经持有某个锁时,它可以再次获取该锁而不会被阻塞。Java中的synchronized关键字和ReentrantLock都是可重入锁。

4.2 为什么需要可重入锁

避免死锁。例如,一个同步方法调用另一个同步方法,如果锁不可重入,线程在调用第二个方法时会等待自己释放锁,导致死锁。

4.3 代码示例

java 复制代码
public class ReentrantLockDemo {
    // synchronized可重入示例
    public synchronized void methodA() {
        System.out.println("执行methodA");
        methodB(); // 同一个线程再次获取锁
    }
    
    public synchronized void methodB() {
        System.out.println("执行methodB");
    }
    
    // ReentrantLock可重入示例
    private static final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
    
    public void methodC() {
        lock.lock();
        try {
            System.out.println("执行methodC,持有锁次数:" + lock.getHoldCount());
            methodD();
        } finally {
            lock.unlock();
            System.out.println("释放methodC的锁,持有锁次数:" + lock.getHoldCount());
        }
    }
    
    public void methodD() {
        lock.lock();
        try {
            System.out.println("执行methodD,持有锁次数:" + lock.getHoldCount());
        } finally {
            lock.unlock();
            System.out.println("释放methodD的锁,持有锁次数:" + lock.getHoldCount());
        }
    }
    
    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        demo.methodA();
        System.out.println("---");
        demo.methodC();
    }
}

4.4 执行结果

复制代码
执行methodA
执行methodB
---
执行methodC,持有锁次数:1
执行methodD,持有锁次数:2
释放methodD的锁,持有锁次数:1
释放methodC的锁,持有锁次数:0

4.5 面试要点

  • 可重入锁通过计数器实现:每次获取锁计数器+1,每次释放锁计数器-1,计数器为0时锁真正释放
  • ReentrantLock支持公平锁和非公平锁(默认非公平),而synchronized只能是非公平锁
  • 公平锁:按照线程等待顺序获取锁;非公平锁:线程直接尝试获取锁,失败才进入等待队列

五、Condition:精确唤醒线程的利器

5.1 核心概念

Condition接口提供了类似Object.wait()/notify()的等待/通知机制,但它可以与Lock配合使用,实现多个等待队列,从而能够精确唤醒指定的线程。

5.2 与Object.wait/notify的对比

特性 Object.wait/notify Condition
关联对象 任意Java对象 与Lock绑定
等待队列 1个 多个
唤醒方式 随机唤醒一个或全部 精确唤醒指定队列的线程
中断支持 支持 支持,还支持不中断等待
超时支持 支持 支持更多超时方式

5.3 代码示例:生产者消费者模型

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private static final Lock lock = new ReentrantLock();
    // 创建两个等待队列:生产者队列和消费者队列
    private static final Condition notFull = lock.newCondition();
    private static final Condition notEmpty = lock.newCondition();
    
    private static final int CAPACITY = 5;
    private static int count = 0;
    
    public static void main(String[] args) {
        // 启动3个生产者和3个消费者
        for (int i = 0; i < 3; i++) {
            new Thread(new Producer(), "Producer-" + i).start();
            new Thread(new Consumer(), "Consumer-" + i).start();
        }
    }
    
    static class Producer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    // 队列满了,生产者等待
                    while (count == CAPACITY) {
                        notFull.await();
                    }
                    count++;
                    System.out.println(Thread.currentThread().getName() + "生产,当前数量:" + count);
                    // 唤醒一个消费者
                    notEmpty.signal();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } finally {
                    lock.unlock();
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    
    static class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    // 队列空了,消费者等待
                    while (count == 0) {
                        notEmpty.await();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "消费,当前数量:" + count);
                    // 唤醒一个生产者
                    notFull.signal();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } finally {
                    lock.unlock();
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

5.4 面试要点

  • await():当前线程进入等待状态,释放锁
  • signal():唤醒一个等待在该Condition上的线程
  • signalAll():唤醒所有等待在该Condition上的线程
  • 必须在lock.lock()lock.unlock()之间调用Condition的方法

六、CountDownLatch:一次性同步工具

6.1 核心概念

CountDownLatch是一个一次性使用的同步工具,它允许一个或多个线程等待其他线程完成一组操作。它通过一个计数器实现,初始值为需要等待的线程数,每个线程完成操作后计数器减1,当计数器变为0时,等待的线程被唤醒。

6.2 常用方法

  • CountDownLatch(int count):构造方法,指定初始计数值
  • countDown():计数器减1,当计数器变为0时唤醒所有等待线程
  • await():当前线程等待,直到计数器变为0
  • await(long timeout, TimeUnit unit):带超时时间的等待

6.3 代码示例:等待多个线程完成任务

java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 5;
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            final int taskId = i;
            new Thread(() -> {
                try {
                    System.out.println("任务" + taskId + "开始执行");
                    TimeUnit.SECONDS.sleep(1 + taskId); // 模拟不同执行时间
                    System.out.println("任务" + taskId + "执行完成");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown(); // 计数器减1
                }
            }).start();
        }
        
        System.out.println("主线程等待所有任务完成...");
        latch.await(); // 等待计数器变为0
        System.out.println("所有任务执行完成,主线程继续执行");
    }
}

6.4 典型应用场景

  • 并行计算:将大任务拆分为多个小任务,等待所有小任务完成后汇总结果
  • 服务启动:等待多个依赖服务启动完成后再提供服务
  • 测试:等待多个线程同时执行某个操作,测试并发性能

6.5 面试要点

  • CountDownLatch是一次性的,计数器变为0后无法重置
  • CyclicBarrier的区别:
    • CountDownLatch:一个线程等待多个线程完成,一次性使用
    • CyclicBarrier:多个线程互相等待,可循环使用

总结

以上是Java多线程面试中最核心的8个知识点,涵盖了线程协作、线程私有变量、线程池、锁机制和并发工具等方面。在面试中,不仅要记住这些概念,更要理解其底层原理和使用场景,并能结合代码示例进行说明。

相关推荐
yaoxin5211231 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道1 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
x***r1511 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
极光代码工作室2 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
XS0301062 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring
NagatoYukee2 小时前
Spring Security基础部分学习
java·学习·spring
彦为君2 小时前
JavaSE-07-异常机制
java·开发语言·后端·python·spring
_Aaron___3 小时前
Spring AI 接入 MCP:工具调用不是“能调就行”,关键是边界治理
java·人工智能·spring