多线程&并发篇面试题

目录

  1. 多线程基础
  2. 线程安全
  3. 锁机制
  4. 并发工具类
  5. 线程池
  6. JUC包
  7. 内存模型
  8. 性能优化

多线程基础

1. 什么是线程?线程和进程的区别?

答案:

线程 :CPU调度的基本单位,是进程内的执行单元。
进程:操作系统资源分配的基本单位。

主要区别:

  • 进程

    • 独立的内存空间
    • 进程间通信复杂(需要IPC机制)
    • 创建和销毁开销大
  • 线程

    • 共享进程内存空间
    • 线程间通信简单(共享内存)
    • 创建和销毁开销小

示例:

java 复制代码
Thread thread = new Thread(() -> System.out.println("Hello Thread"));
thread.start();

2. 创建线程的方式有哪些?

答案:

  1. 继承Thread类
java 复制代码
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
  1. 实现Runnable接口
java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}
  1. 实现Callable接口
java 复制代码
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result";
    }
}
  1. 使用线程池
java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("Pool thread"));

3. 线程的生命周期?

答案:

线程状态:

  1. NEW(新建)- 线程创建但未启动
  2. RUNNABLE(可运行)- 线程正在JVM中执行
  3. BLOCKED(阻塞)- 线程被阻塞等待监视器锁
  4. WAITING(等待)- 线程无限期等待另一个线程执行特定操作
  5. TIMED_WAITING(超时等待)- 线程等待另一个线程执行操作,但有时限
  6. TERMINATED(终止)- 线程执行完毕

状态转换:

复制代码
NEW → RUNNABLE ⇄ BLOCKED/WAITING/TIMED_WAITING → TERMINATED

4. 线程的优先级?

答案:

优先级范围:1-10,默认优先级为5。

设置优先级:

java 复制代码
thread.setPriority(Thread.MAX_PRIORITY);  // 设置最高优先级(10)
thread.setPriority(Thread.MIN_PRIORITY);   // 设置最低优先级(1)
thread.setPriority(Thread.NORM_PRIORITY);  // 设置默认优先级(5)

注意事项:

  • 优先级只是建议,不保证执行顺序
  • 最终执行顺序依赖操作系统调度
  • 不要过度依赖线程优先级控制程序逻辑

线程安全

5. 什么是线程安全?

答案:

线程安全 指多线程环境下,程序能够正确处理共享数据,不会出现数据不一致或竞态条件。

线程安全级别:

  1. 不可变(Immutable)- String、Integer等不可变对象
  2. 绝对线程安全(Absolutely Thread-Safe)- Vector、Hashtable等
  3. 相对线程安全(Relatively Thread-Safe)- 大部分JDK类,如ArrayList、HashMap
  4. 线程兼容(Thread-Compatible)- 需要外部同步
  5. 线程对立(Thread-Hostile)- 无论是否同步都无法在多线程环境使用

6. 什么是竞态条件?

答案: 竞态条件 指多个线程访问共享资源时,执行结果依赖于线程执行的时序。

示例:

java 复制代码
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // 非原子操作,存在竞态条件
    }
}

解决方案: 使用synchronized、volatile、原子类等同步机制。

7. 什么是死锁?如何避免死锁?

答案:

死锁 指两个或多个线程互相等待对方释放资源,导致程序无法继续执行。

死锁的四个必要条件:

  1. 互斥条件:资源不能被多个线程同时使用
  2. 请求和保持条件:线程持有资源的同时请求其他资源
  3. 不剥夺条件:资源不能被强制释放
  4. 循环等待条件:线程形成循环等待链

避免死锁的方法:

  1. 避免嵌套锁:尽量不要在持有一个锁的情况下获取另一个锁
  2. 按顺序获取锁:所有线程按照相同的顺序获取锁
  3. 使用超时锁 :使用tryLock(timeout)避免无限等待
  4. 死锁检测和恢复:定期检测死锁并采取恢复措施

示例:

java 复制代码
// 按固定顺序获取锁,避免死锁
synchronized(lock1) {
    synchronized(lock2) {
        // 临界区代码
    }
}

锁机制

8. synchronized关键字的作用?

答案:

synchronized 是Java内置的同步机制,用于实现线程安全,确保同一时刻只有一个线程执行同步代码。

三种使用方式:

  1. 同步方法
java 复制代码
public synchronized void method() {
    // 同步方法
}
  1. 同步代码块
java 复制代码
synchronized(object) {
    // 同步代码块
}
  1. 静态同步方法
java 复制代码
public static synchronized void method() {
    // 静态同步方法
}

锁对象说明:

  • 实例方法 :锁对象是this(当前实例对象)
  • 静态方法 :锁对象是Class对象(类对象)
  • 同步代码块:锁对象是括号中指定的对象

9. synchronized的底层原理?

答案:

底层实现 :基于JVM的monitor(监视器)机制,通过monitorentermonitorexit指令实现。

锁升级过程

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

各级锁的特点:

  • 偏向锁:只有一个线程访问时,记录线程ID,避免CAS操作,性能最好
  • 轻量级锁:多线程竞争不激烈时,使用CAS操作,避免操作系统互斥量
  • 重量级锁:多线程竞争激烈时,使用操作系统互斥量(Mutex),会导致线程阻塞

10. volatile关键字的作用?

答案:

volatile 保证变量的可见性和有序性,但不保证原子性。

三大特性:

  1. 可见性:一个线程修改volatile变量,其他线程立即可见
  2. 有序性:禁止指令重排序(通过内存屏障实现)
  3. 不保证原子性:复合操作(如i++)仍需要同步

使用场景:

  • 状态标志位
  • 双重检查锁定(DCL单例模式)
  • 独立观察变量

示例:

java 复制代码
private volatile boolean flag = false;

// 线程1
public void setFlag() {
    flag = true;  // 写操作对其他线程立即可见
}

// 线程2
public void checkFlag() {
    if (flag) {  // 能立即看到线程1的修改
        // 执行逻辑
    }
}

11. ReentrantLock和synchronized的区别?

答案:

ReentrantLock 是JUC包提供的可重入锁,相比synchronized提供了更多高级功能。

详细区别对比:

特性 synchronized ReentrantLock
锁获取 自动获取和释放 手动获取和释放
中断响应 不支持 支持
超时获取 不支持 支持
公平锁 非公平 可选择公平或非公平
条件变量 单一条件 多个条件
性能 JVM优化,性能好 需要手动优化

示例:

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

12. 什么是读写锁?

答案:

读写锁(ReentrantReadWriteLock)允许多个线程同时读取,但写操作是独占的。

核心特性:

  • 读锁(共享锁):多个线程可以同时持有读锁
  • 写锁(排他锁):只有一个线程可以持有写锁,且写锁排斥所有读锁

使用示例:

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作
rwLock.readLock().lock();
try {
    // 读取数据
} finally {
    rwLock.readLock().unlock();
}

// 写操作
rwLock.writeLock().lock();
try {
    // 修改数据
} finally {
    rwLock.writeLock().unlock();
}

使用场景:读多写少的场景,如缓存、配置管理等。

优势:提高并发性能,多个读操作可以并发执行,不会相互阻塞。


并发工具类

13. CountDownLatch详解(源码+应用场景)

答案:

CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。它基于AQS(AbstractQueuedSynchronizer)的共享锁模式实现。

核心特性:

  • 一次性使用:计数到达零后不能重置
  • 共享锁模式:多个线程可以同时等待
  • 倒计时功能:从初始计数开始递减到0

核心方法:

  • await():等待计数器到达0
  • countDown():计数器减1
  • getCount():获取当前计数

基础示例:

java 复制代码
// 创建CountDownLatch,初始计数为3
CountDownLatch latch = new CountDownLatch(3);

// 工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " 正在执行任务");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 任务完成");
        } finally {
            latch.countDown();  // 计数器减1
        }
    }).start();
}

// 主线程等待
System.out.println("主线程等待所有任务完成...");
latch.await();  // 阻塞,直到计数器为0
System.out.println("所有任务完成,主线程继续执行");

典型应用场景:

场景1:系统初始化等待

java 复制代码
public class SystemInitializer {
    private final CountDownLatch initLatch = new CountDownLatch(3);
    
    public void initialize() throws InterruptedException {
        // 初始化数据库
        new Thread(() -> {
            try {
                System.out.println("初始化数据库...");
                Thread.sleep(2000);
                System.out.println("数据库初始化完成");
            } finally {
                initLatch.countDown();
            }
        }).start();
        
        // 初始化缓存
        new Thread(() -> {
            try {
                System.out.println("初始化缓存...");
                Thread.sleep(1500);
                System.out.println("缓存初始化完成");
            } finally {
                initLatch.countDown();
            }
        }).start();
        
        // 初始化配置
        new Thread(() -> {
            try {
                System.out.println("加载配置...");
                Thread.sleep(1000);
                System.out.println("配置加载完成");
            } finally {
                initLatch.countDown();
            }
        }).start();
        
        // 等待所有初始化完成
        initLatch.await();
        System.out.println("系统初始化完成,可以对外提供服务");
    }
}

场景2:并发测试(模拟高并发)

java 复制代码
public class ConcurrentTest {
    public void testConcurrent() throws InterruptedException {
        int threadCount = 100;
        CountDownLatch startLatch = new CountDownLatch(1);  // 开始信号
        CountDownLatch endLatch = new CountDownLatch(threadCount);  // 完成信号
        
        // 创建100个线程
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    startLatch.await();  // 等待开始信号
                    // 执行并发测试
                    performTest();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    endLatch.countDown();
                }
            }).start();
        }
        
        // 所有线程就位后,同时启动
        System.out.println("所有线程准备就绪,开始并发测试");
        startLatch.countDown();  // 释放开始信号
        
        // 等待所有线程完成
        endLatch.await();
        System.out.println("并发测试完成");
    }
    
    private void performTest() {
        // 测试逻辑
    }
}

源码实现原理(简化版):

java 复制代码
public class CountDownLatch {
    // 内部同步器,基于AQS
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);  // 设置初始计数
        }
        
        // 尝试获取共享锁:计数为0返回1,否则返回-1
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        
        // 尝试释放共享锁:递减计数,为0时返回true
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0) return false;  // 已经为0,不能继续减
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))  // CAS更新计数
                    return nextc == 0;  // 返回是否到达0
            }
        }
    }
    
    private final Sync sync;
    
    public CountDownLatch(int count) {
        this.sync = new Sync(count);
    }
    
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);  // 获取共享锁
    }
    
    public void countDown() {
        sync.releaseShared(1);  // 释放共享锁,计数减1
    }
}

vs wait/notify 的优势:

  1. 代码简洁:无需手动管理同步代码块和条件判断
  2. 线程安全:内置线程安全保证,无需额外同步
  3. 无虚假唤醒:不会出现wait/notify的虚假唤醒问题
  4. 性能更好:基于AQS的无锁算法,性能优于synchronized
  5. 易于维护:代码清晰,不易出错

14. CyclicBarrier的作用?

答案:

CyclicBarrier(循环屏障)是一个同步辅助类,允许多个线程相互等待,直到所有线程都到达某个公共屏障点。

核心特性:

  • 可重复使用:所有线程到达屏障后,屏障会重置,可以重复使用
  • 相互等待:所有线程必须到达屏障点才能继续执行
  • 回调函数:可以设置所有线程到达后执行的回调动作

与CountDownLatch区别:

特性 CountDownLatch CyclicBarrier
使用次数 一次性 可重复使用
计数方向 递减到0 递增到指定值
等待模式 一个或多个线程等待 多个线程相互等待
重置能力 不支持 支持reset()

示例:

java 复制代码
// 创建CyclicBarrier,3个线程到达后执行回调
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达屏障,执行汇总任务");
});

// 工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " 开始执行");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 到达屏障");
            barrier.await();  // 等待其他线程到达
            System.out.println(Thread.currentThread().getName() + " 继续执行");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

15. Semaphore的作用?

答案:

Semaphore(信号量)是一个计数信号量,用于控制同时访问特定资源的线程数量。

核心特性:

  • 许可控制:通过许可证(permits)控制并发访问数量
  • 公平性:支持公平和非公平模式
  • 可中断:支持中断和超时获取

使用场景:

  • 限制并发访问数量(如数据库连接池)
  • 实现资源池(如线程池、对象池)
  • 控制流量(如限流器)

示例:

java 复制代码
// 创建Semaphore,允许3个线程同时访问
Semaphore semaphore = new Semaphore(3);

// 访问资源
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire();  // 获取许可
            System.out.println(Thread.currentThread().getName() + " 获取许可,开始访问资源");
            Thread.sleep(2000);  // 模拟资源访问
            System.out.println(Thread.currentThread().getName() + " 释放许可");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();  // 释放许可
        }
    }).start();
}

16. Exchanger的作用?

答案:

Exchanger 是一个同步点,用于两个线程之间交换数据。

核心特性:

  • 双向交换:两个线程互相交换数据
  • 同步点:两个线程必须同时到达才能交换
  • 类型安全:泛型支持,保证类型安全

使用场景:

  • 两个线程需要交换数据
  • 生产者消费者模式
  • 数据校验(两个线程处理相同数据,交换结果进行校验)

示例:

java 复制代码
Exchanger<String> exchanger = new Exchanger<>();

// 线程1
new Thread(() -> {
    try {
        String data = "来自线程1的数据";
        System.out.println("线程1准备交换数据:" + data);
        String received = exchanger.exchange(data);  // 交换数据,阻塞等待线程2
        System.out.println("线程1收到数据:" + received);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 线程2
new Thread(() -> {
    try {
        String data = "来自线程2的数据";
        System.out.println("线程2准备交换数据:" + data);
        String received = exchanger.exchange(data);  // 交换数据,阻塞等待线程1
        System.out.println("线程2收到数据:" + received);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

线程池

17. 什么是线程池?为什么使用线程池?

答案:

线程池 是预先创建一定数量的线程,用于执行任务的管理机制。

四大优势:

  1. 降低资源消耗

    • 避免频繁创建和销毁线程
    • 减少内存开销和GC压力
  2. 提高响应速度

    • 任务到达时无需等待线程创建
    • 线程已经就绪,可以立即执行
  3. 提高线程可管理性

    • 统一管理线程数量、状态
    • 避免线程数量失控
  4. 提供更多功能

    • 支持定时执行、定期执行
    • 支持任务队列、拒绝策略等

18. 线程池的核心参数?

答案:

ThreadPoolExecutor七大核心参数:

  1. corePoolSize(核心线程数)

    • 线程池中始终保持活跃的线程数量
    • 即使线程空闲也不会被销毁
  2. maximumPoolSize(最大线程数)

    • 线程池中允许的最大线程数量
    • 当队列满时,会创建新线程直到达到最大值
  3. keepAliveTime(线程空闲时间)

    • 非核心线程的空闲存活时间
    • 超过此时间的空闲线程会被销毁
  4. unit(时间单位)

    • keepAliveTime的时间单位
    • TimeUnit枚举值(SECONDS、MINUTES等)
  5. workQueue(工作队列)

    • 存储待执行任务的阻塞队列
    • 常用:LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue
  6. threadFactory(线程工厂)

    • 用于创建新线程
    • 可以自定义线程名称、优先级等
  7. handler(拒绝策略)

    • 当队列满且线程数达到最大值时的处理策略
    • 四种默认策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,  // 核心线程数
    10, // 最大线程数
    60L, // 空闲时间
    TimeUnit.SECONDS, // 时间单位
    new LinkedBlockingQueue<>(100), // 工作队列
    new ThreadFactory() {
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "MyPool-thread-" + threadNumber.getAndIncrement());
            t.setDaemon(false);
            t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }, 
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

19. 线程池的拒绝策略?

答案:

当线程池无法接受新任务时(队列满且线程数达到最大值),会触发拒绝策略。

四种默认拒绝策略:

  1. AbortPolicy(默认策略)

    • 直接抛出RejectedExecutionException异常
    • 让调用者感知到任务被拒绝
  2. CallerRunsPolicy(调用者运行策略)

    • 由调用线程(提交任务的线程)直接执行任务
    • 降低任务提交速度,起到负反馈作用
  3. DiscardPolicy(丢弃策略)

    • 静默丢弃任务,不抛出异常
    • 可能导致任务丢失,需谨慎使用
  4. DiscardOldestPolicy(丢弃最老任务策略)

    • 丢弃队列中最老的任务
    • 然后重新提交当前任务

自定义拒绝策略:

java 复制代码
new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义处理逻辑
        // 例如:记录日志、存入数据库、放入MQ等
        System.err.println("任务被拒绝: " + r.toString());
        // 可以选择性地处理任务
    }
}

20. 常见的线程池类型?

答案:

Executors提供的四种线程池:

  1. newFixedThreadPool(固定大小线程池)

    • 核心线程数 = 最大线程数
    • 使用无界队列LinkedBlockingQueue
    • 适合:负载较重的服务器
  2. newCachedThreadPool(缓存线程池)

    • 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE
    • 使用SynchronousQueue(不存储任务)
    • 适合:执行大量短期异步任务
  3. newSingleThreadExecutor(单线程池)

    • 只有一个线程的线程池
    • 保证任务按提交顺序串行执行
    • 适合:需要顺序执行的场景
  4. newScheduledThreadPool(定时任务线程池)

    • 支持定时及周期性任务执行
    • 适合:定时任务场景

⚠️ 重要提示:

生产环境不推荐使用Executors创建线程池,原因:

  • newFixedThreadPoolnewSingleThreadExecutor使用无界队列,可能导致OOM
  • newCachedThreadPool最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM

**推荐做法:**使用ThreadPoolExecutor自定义参数,明确指定队列大小和最大线程数。


JUC包

21. 什么是CAS?CAS的ABA问题?

答案:

CAS(Compare And Swap)是一种无锁算法,通过比较和交换实现原子操作。

CAS三要素:

  • 内存位置(V):要更新的变量
  • 预期原值(A):期望的当前值
  • 新值(B):要设置的新值

CAS操作流程:

java 复制代码
// 伪代码
boolean compareAndSwap(V, A, B) {
    if (V == A) {  // 如果当前值等于期望值
        V = B;     // 则更新为新值
        return true;
    }
    return false;  // 否则更新失败
}

// 实际使用
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.compareAndSet(0, 1);  // 期望值为0,更新为1

ABA问题:

  • 问题描述:变量从A变为B,再变回A,CAS认为没有变化,但实际已经被修改过
  • 影响场景:链表、栈等数据结构操作

解决方案:

  1. 使用版本号:AtomicStampedReference(带版本戳的原子引用)
java 复制代码
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(0, 0);
int stamp = atomicRef.getStamp();
atomicRef.compareAndSet(0, 1, stamp, stamp + 1);  // 同时比较值和版本号
  1. 使用时间戳:AtomicMarkableReference(带标记的原子引用)

22. 原子类的实现原理?

答案: 原子类 基于CAS操作实现,如AtomicInteger、AtomicLong等。

实现原理: 使用Unsafe类的CAS操作,底层调用CPU的CAS指令。

示例:

java 复制代码
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子递增
atomicInt.compareAndSet(0, 1); // CAS操作

23. ConcurrentHashMap的实现原理?

答案: ConcurrentHashMap 是线程安全的HashMap实现。

JDK 1.7实现: 使用分段锁(Segment),每个Segment包含一个HashEntry数组。

JDK 1.8实现: 使用CAS + synchronized,数组 + 链表 + 红黑树结构。

优势: 高并发性能,读操作无锁,写操作使用CAS和synchronized。

24. BlockingQueue的实现原理?

答案: BlockingQueue 是支持阻塞操作的队列接口。

常见实现: 1. ArrayBlockingQueue (有界数组队列) 2. LinkedBlockingQueue (有界链表队列) 3. PriorityBlockingQueue (优先级队列) 4. DelayQueue (延迟队列) 5. SynchronousQueue (同步队列)

阻塞操作:

java 复制代码
put()    // 阻塞插入
take()   // 阻塞获取
offer()  // 非阻塞插入
poll()   // 非阻塞获取

内存模型

25. 什么是JMM?

答案:

JMM(Java Memory Model,Java内存模型)定义了Java程序中各种变量的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

内存区域:

  • 主内存(Main Memory):所有线程共享,存储所有变量
  • 工作内存(Working Memory):每个线程私有,存储该线程使用的变量副本

八种内存交互操作:

  1. lock(锁定):作用于主内存的变量
  2. unlock(解锁):作用于主内存的变量
  3. read(读取):从主内存读取变量到工作内存
  4. load(载入):将read的变量值放入工作内存的变量副本
  5. use(使用):从工作内存读取变量值
  6. assign(赋值):将新值赋给工作内存的变量
  7. store(存储):将工作内存的变量值传送到主内存
  8. write(写入):将store的变量值写入主内存

26. 什么是happens-before?

答案:

happens-before 是JMM定义的内存可见性规则,用于确定操作之间的内存可见性。

核心含义:

如果操作A happens-before 操作B,那么A的执行结果对B可见,且A的执行顺序排在B之前。

八大happens-before规则:

  1. 程序顺序规则(Program Order Rule)

    • 同一个线程内,前面的操作 happens-before 后面的操作
  2. 监视器锁规则(Monitor Lock Rule)

    • unlock操作 happens-before 后续对同一个锁的lock操作
  3. volatile变量规则(Volatile Variable Rule)

    • volatile写操作 happens-before 后续对该变量的读操作
  4. 线程启动规则(Thread Start Rule)

    • Thread.start() happens-before 该线程的每一个动作
  5. 线程终止规则(Thread Termination Rule)

    • 线程的所有操作 happens-before 其他线程检测到该线程终止
  6. 线程中断规则(Thread Interruption Rule)

    • 线程interrupt()调用 happens-before 被中断线程检测到中断事件
  7. 对象终结规则(Finalizer Rule)

    • 对象的构造函数执行结束 happens-before finalize()方法
  8. 传递性(Transitivity)

    • 如果A happens-before B,B happens-before C,则A happens-before C

27. 什么是内存屏障?

答案:

内存屏障(Memory Barrier)是CPU指令,用于防止指令重排序和保证内存可见性。

四种类型:

  1. LoadLoad屏障(读-读屏障)

    • 确保Load1的数据装载先于Load2及后续装载指令
    • 格式:Load1; LoadLoad; Load2
  2. StoreStore屏障(写-写屏障)

    • 确保Store1的数据刷新到主内存先于Store2及后续存储指令
    • 格式:Store1; StoreStore; Store2
  3. LoadStore屏障(读-写屏障)

    • 确保Load1的数据装载先于Store2及后续存储指令
    • 格式:Load1; LoadStore; Store2
  4. StoreLoad屏障(写-读屏障)

    • 确保Store1的数据刷新到主内存先于Load2及后续装载指令
    • 格式:Store1; StoreLoad; Load2
    • 开销最大的屏障

作用:

  • 防止指令重排序
  • 保证内存可见性
  • 确保并发程序的正确性

性能优化

28. 如何优化多线程性能?

答案:

六大优化策略:

  1. 减少锁的粒度

    • 使用细粒度锁替代粗粒度锁
    • 例如:ConcurrentHashMap的分段锁
  2. 减少锁的持有时间

    • 尽快释放锁,缩小同步代码块范围
    • 将耗时操作移到锁外执行
  3. 使用无锁数据结构

    • 使用CAS操作替代锁
    • 使用原子类(AtomicInteger等)
    • 使用并发容器(ConcurrentHashMap等)
  4. 合理使用线程池

    • 避免频繁创建和销毁线程
    • 根据任务特性选择合适的线程池配置
  5. 使用读写锁

    • 读多写少场景使用ReentrantReadWriteLock
    • 允许多个读操作并发执行
  6. 避免锁竞争

    • 减少同步代码块
    • 使用ThreadLocal避免共享
    • 使用不可变对象

29. 什么是伪共享?如何避免?

答案:

伪共享(False Sharing)指多个线程访问同一缓存行(Cache Line)的不同变量,导致缓存行在CPU核心之间频繁同步,严重影响性能。

产生原因:

  • CPU缓存行通常是64字节
  • 多个变量可能位于同一缓存行
  • 一个变量被修改,整个缓存行失效,影响其他变量

避免方法:

  1. 使用@Contended注解(JDK 8+)
java 复制代码
public class PaddedData {
    @Contended  // 填充缓存行,避免伪共享
    public volatile long value1;
    
    @Contended
    public volatile long value2;
}
  1. 手动填充字节
java 复制代码
public class PaddedData {
    public volatile long value;
    private long p1, p2, p3, p4, p5, p6, p7;  // 填充56字节
}
  1. 重新排列字段

    • 将可能被不同线程访问的字段分开
    • 避免它们位于同一缓存行
  2. 使用局部变量

    • 减少对共享变量的访问
    • 使用局部变量累加,最后再写回共享变量

30. 如何调试多线程问题?

答案:

五种调试方法:

  1. 使用日志

    • 记录线程执行状态、时间戳
    • 使用线程名称标识不同线程
    • 记录关键操作和状态变化
  2. 使用调试器

    • 设置断点,观察变量状态
    • 查看线程堆栈信息
    • 单步执行,观察执行流程
  3. 使用线程转储(Thread Dump)

    • 使用jstack命令获取线程堆栈
    • 分析死锁、线程状态等问题
    • 命令:jstack <pid> > thread_dump.txt
  4. 使用性能分析工具

    • JProfiler:专业的性能分析工具
    • VisualVM:JDK自带的可视化工具
    • JConsole:监控线程、内存、CPU等
  5. 使用并发测试工具

    • JCStress:并发压力测试工具
    • ThreadSanitizer:线程安全检测工具
    • FindBugs/SpotBugs:静态代码分析

常见问题及排查:

  • 死锁:使用jstack查看线程堆栈
  • 活锁:观察线程状态变化
  • 饥饿:检查线程优先级和调度
  • 竞态条件:使用断点调试,观察共享变量
  • 内存泄漏:使用内存分析工具(MAT、JProfiler)

31. 多线程环境下如何避免内存泄露?

答案: 内存泄露 指程序在运行过程中,不再使用的对象无法被垃圾回收器回收,导致内存占用持续增长。

多线程环境下的内存泄露原因:

  1. 线程未正确关闭
java 复制代码
// 错误示例:线程未正确关闭
Thread thread = new Thread(() -> {
    while (true) {
        // 长时间运行的任务
    }
});
thread.start();
// 忘记调用 thread.interrupt() 或设置停止标志
  1. 线程池未正确关闭
java 复制代码
// 错误示例:线程池未关闭
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    // 任务执行
});
// 忘记调用 executor.shutdown()
  1. 监听器未移除
java 复制代码
// 错误示例:监听器未移除
public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
    
    // 缺少移除监听器的方法
    // public void removeListener(EventListener listener) {
    //     listeners.remove(listener);
    // }
}
  1. 静态集合持有对象引用
java 复制代码
// 错误示例:静态集合持有对象引用
public class CacheManager {
    private static Map<String, Object> cache = new HashMap<>();
    
    public void put(String key, Object value) {
        cache.put(key, value); // 对象永远不会被回收
    }
    
    // 缺少清理方法
}

避免内存泄露的方法:

  1. 正确管理线程生命周期
java 复制代码
// 正确示例:使用标志位控制线程停止
public class WorkerThread extends Thread {
    private volatile boolean running = true;
    
    @Override
    public void run() {
        while (running) {
            // 执行任务
        }
    }
    
    public void stopWorker() {
        running = false;
    }
}

// 使用
WorkerThread worker = new WorkerThread();
worker.start();
// 需要停止时
worker.stopWorker();
  1. 正确关闭线程池
java 复制代码
// 正确示例:正确关闭线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
try {
    executor.submit(() -> {
        // 任务执行
    });
} finally {
    executor.shutdown(); // 关闭线程池
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow(); // 强制关闭
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
    }
}
  1. 使用弱引用
java 复制代码
// 正确示例:使用弱引用避免内存泄露
public class WeakReferenceCache {
    private Map<String, WeakReference<Object>> cache = new HashMap<>();
    
    public void put(String key, Object value) {
        cache.put(key, new WeakReference<>(value));
    }
    
    public Object get(String key) {
        WeakReference<Object> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }
}
  1. 及时清理资源
java 复制代码
// 正确示例:及时清理资源
public class ResourceManager {
    private List<Resource> resources = new ArrayList<>();
    
    public void addResource(Resource resource) {
        resources.add(resource);
    }
    
    public void removeResource(Resource resource) {
        resources.remove(resource);
        resource.close(); // 及时关闭资源
    }
    
    public void cleanup() {
        for (Resource resource : resources) {
            resource.close();
        }
        resources.clear();
    }
}
  1. 使用ThreadLocal的正确方式
java 复制代码
// 正确示例:正确使用ThreadLocal
public class ThreadLocalManager {
    private static final ThreadLocal<SimpleDateFormat> dateFormat = 
        new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd");
            }
        };
    
    public static String formatDate(Date date) {
        return dateFormat.get().format(date);
    }
    
    // 在适当的时候清理ThreadLocal
    public static void cleanup() {
        dateFormat.remove();
    }
}
  1. 避免循环引用
java 复制代码
// 正确示例:避免循环引用
public class Node {
    private String data;
    private Node next;
    
    public void setNext(Node next) {
        this.next = next;
    }
    
    // 提供清理方法
    public void clear() {
        this.next = null;
        this.data = null;
    }
}

内存泄露检测工具:

  • JProfiler - 专业的内存分析工具
  • VisualVM - JDK自带的性能分析工具
  • MAT (Memory Analyzer Tool) - Eclipse内存分析工具
  • JConsole - JDK自带的监控工具

最佳实践:

  1. 定期检查 - 使用工具定期检查内存使用情况
  2. 代码审查 - 在代码审查中重点关注资源管理
  3. 单元测试 - 编写测试验证资源正确释放
  4. 监控告警 - 在生产环境设置内存使用告警

总结

多线程和并发编程是Java开发中的重要技能,掌握线程安全、锁机制、并发工具类、线程池等核心概念对于编写高性能、高并发的应用程序至关重要。

重点掌握: 线程安全机制、锁的使用和选择、并发工具类的应用、线程池的配置和优化、JMM内存模型、性能优化技巧

通过深入理解这些概念和原理,能够在面试中展现出扎实的多线程编程功底。

相关推荐
用户298698530144 小时前
Java高效读取CSV文件的方法与分步实例
java·后端
南北是北北4 小时前
RecyclerView 的数据驱动更新
面试
uhakadotcom4 小时前
coze的AsyncTokenAuth和coze的TokenAuth有哪些使用的差异?
后端·面试·github
Chejdj4 小时前
StateFlow、SharedFlow 和LiveData区别
android·面试
程序员卷卷狗5 小时前
JVM实战:从内存模型到性能调优的全链路剖析
java·jvm·后端·性能优化·架构
Android-Flutter5 小时前
kotlin - 正则表达式,识别年月日
java·kotlin
得物技术5 小时前
线程池ThreadPoolExecutor源码深度解析|得物技术
java·编译器·dns
道可到5 小时前
直接可以拿来的面经 | 从JDK 8到JDK 21:一次团队升级的实战经验与价值复盘
java·面试·架构
南北是北北5 小时前
RecyclerView 进阶绑定:多类型 / 局部刷新(payload)/ 稳定 ID
面试