Java并发编程面试题大全

Java并发编程面试题大全

一、并发编程基础

1.1 什么是并发编程?

答案:
并发编程是指在一个程序中同时执行多个独立的任务。它允许程序的不同部分看似同时运行,从而提高程序的执行效率和资源利用率。在Java中,并发编程主要通过线程来实现。

并发编程的主要特点包括:

  1. 多任务同时执行
  2. 资源共享和同步
  3. 线程间的通信和协调

1.2 串行与并行的区别是什么?

答案:
串行和并行是两种不同的任务执行方式:

  1. 串行(Sequential)
    • 任务按顺序一个接一个地执行
    • 前一个任务完成后才开始下一个任务
    • 单线程环境下的执行方式
    • 简单但效率较低
  2. 并行(Parallel)
    • 多个任务同时执行
    • 需要多核CPU或多处理器支持
    • 真正的多任务同时执行
    • 效率高但编程复杂度增加

关键区别在于:并行是真正的多任务同时执行(需要多核支持),而并发是通过时间片轮转实现的"看似同时"执行。

1.3 并发编程的目的是什么?

答案:
并发编程的主要目的包括:

  1. 提高性能:充分利用多核CPU资源,加快程序执行速度
  2. 提高响应性:避免长时间操作阻塞主线程,保持UI响应
  3. 简化建模:某些问题(如服务器处理多个客户端请求)用并发模型更自然
  4. 资源利用:在等待I/O操作时,CPU可以执行其他任务
  5. 公平性:多个用户或任务可以公平地共享资源

1.4 什么时候适合使用并发编程?

答案:
适合使用并发编程的场景包括:

  1. CPU密集型任务:需要大量计算且可分解为独立子任务
  2. I/O密集型任务:程序经常等待I/O操作(如网络、磁盘)
  3. 需要高响应性:如GUI程序需要保持界面响应
  4. 多用户/多客户端:如Web服务器处理并发请求
  5. 异步处理:后台执行不影响主流程的任务

不适合的场景:

  1. 任务间有强依赖关系
  2. 共享资源频繁访问且难以同步
  3. 任务非常简单且执行时间很短

二、线程基础

2.1 进程与线程的区别是什么?

答案:
进程和线程的主要区别:

比较点进程线程
定义操作系统资源分配的基本单位CPU调度的基本单位
内存有独立的内存空间共享进程的内存空间
开销创建、切换开销大创建、切换开销小
通信进程间通信(IPC)复杂线程间通信简单(共享内存)
稳定性一个进程崩溃不影响其他进程一个线程崩溃可能导致整个进程崩溃
资源系统分配资源给进程线程共享进程资源

简单来说:进程是"程序的一次执行",线程是"进程中的一个执行路径"。

2.2 线程的状态及其相互转换

答案:
Java线程有以下6种状态:

  1. NEW(初始状态):线程被创建但尚未启动
  2. RUNNABLE(可运行状态):线程正在JVM中执行或等待操作系统资源
  3. BLOCKED(阻塞状态):线程等待获取监视器锁(进入synchronized块)
  4. WAITING(等待状态):无限期等待其他线程执行特定操作(无超时)
  5. TIMED_WAITING(超时等待):有限期等待(有超时参数)
  6. TERMINATED(终止状态):线程执行完毕

状态转换图:

NEW → start() → RUNNABLE
RUNNABLE → 获取锁 → BLOCKED
RUNNABLE → wait() → WAITING
RUNNABLE → sleep(ms) → TIMED_WAITING
WAITING/TIMED_WAITING → notify()/notifyAll() → RUNNABLE
RUNNABLE → run()结束 → TERMINATED

2.3 创建线程的两种方式

答案:

  1. 继承Thread类
class MyThread extends Thread {
    public void run() {
        // 线程执行代码
    }
}
// 使用
MyThread t = new MyThread();
t.start();
  1. 实现Runnable接口
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行代码
    }
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();

推荐使用实现Runnable接口的方式,因为:

  • Java不支持多重继承,继承Thread类后不能再继承其他类
  • 实现Runnable接口更符合面向对象设计
  • 线程池只能接受Runnable或Callable任务

2.4 线程挂起和恢复

答案:

  1. 挂起线程
    • 早期使用suspend()方法,但已废弃,因为它容易导致死锁
    • 现代做法:使用wait()或条件变量让线程等待
  2. 恢复线程
    • 早期使用resume()方法,也已废弃
    • 现代做法:使用notify()/notifyAll()或条件变量唤醒线程

注意:suspend()和resume()方法已被废弃,不应再使用,因为:

  • suspend()挂起线程但不释放锁,容易导致死锁
  • resume()如果意外先于suspend()调用,可能导致线程永远挂起

2.5 线程的中断操作

答案:

正确中断线程的三种方式:

  1. 使用interrupt()方法
Thread t = new Thread(() -> {
    while(!Thread.currentThread().isInterrupted()) {
        // 工作代码
    }
});
t.start();
// 中断线程
t.interrupt();
  1. 使用boolean标志位(volatile修饰)
class MyRunnable implements Runnable {
    private volatile boolean stopped = false;
    
    public void run() {
        while(!stopped) {
            // 工作代码
        }
    }
    
    public void stop() {
        stopped = true;
    }
}
  1. 通过Future取消
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    // 长时间任务
});
future.cancel(true); // 中断线程

注意:stop()方法已废弃,因为它会强制终止线程,可能导致资源未释放或数据不一致。

2.6 线程的优先级

答案:

  1. 优先级介绍
    • Java线程优先级范围:1(MIN_PRIORITY)到10(MAX_PRIORITY)
    • 默认优先级:5(NORM_PRIORITY)
    • 高优先级线程更可能被调度执行,但不保证
  2. 设置优先级
Thread t = new Thread();
t.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级

注意

  • 优先级只是给调度器的建议,不保证执行顺序
  • 不同操作系统对优先级的处理可能不同
  • 过度依赖优先级可能导致线程饥饿问题

2.7 守护线程

答案:

  1. 线程分类
    • 用户线程:主线程和用户创建的线程默认都是用户线程
    • 守护线程:为其他线程提供服务的后台线程
  2. 守护线程特点
    • JVM在所有用户线程结束后退出,不管守护线程是否还在运行
    • 守护线程通常用于执行后台任务,如垃圾回收、心跳检测等
  3. 设置守护线程
Thread daemonThread = new Thread();
daemonThread.setDaemon(true); // 必须在start()前调用
daemonThread.start();

注意

  • 守护线程中finally块不保证执行
  • 守护线程创建的子线程也是守护线程
  • 不要将I/O操作等关键任务放在守护线程中

三、线程安全性

3.1 什么是线程安全?

答案:
线程安全是指当多个线程访问某个类或对象时,这个类或对象始终能表现出正确的行为,无需额外的同步或协调。

线程安全的三个核心概念:

  1. 原子性:操作不可中断,要么全部执行,要么都不执行
  2. 可见性:一个线程对共享变量的修改能立即被其他线程看到
  3. 有序性:程序执行的顺序按照代码的先后顺序执行

实现线程安全的常见方法:

  • 使用synchronized关键字
  • 使用volatile关键字
  • 使用原子类(AtomicInteger等)
  • 使用锁(Lock接口实现类)
  • 使用线程安全的容器

3.2 从字节码角度剖析线程不安全操作

答案:
以i++操作为例,它不是原子操作,从字节码看分为三步:

  1. getfield:获取当前值
  2. iconst_1:加载常量1
  3. iadd:执行加法
  4. putfield:写回新值

在多线程环境下,可能出现以下情况:

线程A:getfield (i=0)
线程B:getfield (i=0)
线程A:iadd (0+1=1)
线程B:iadd (0+1=1)
线程A:putfield (i=1)
线程B:putfield (i=1) // 结果应该是2,实际是1

这就是典型的竞态条件问题,需要通过同步机制解决。

3.3 原子性操作

答案:

  1. 什么是原子性
    原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行,不会出现部分执行的情况。

  2. 如何实现原子性

    • 使用synchronized关键字
    public synchronized void increment() {
        count++;
    }
    
    • 使用原子类
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet();
    }
    
    • 使用Lock
    private Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    

3.4 深入理解synchronized关键字

答案:

  1. 内置锁(监视器锁)

    • 每个Java对象都有一个内置锁
    • synchronized使用对象的内置锁实现同步
    • 进入同步代码块前自动获取锁,退出时自动释放
  2. 互斥锁特性

    • 互斥性:同一时刻只有一个线程能持有锁
    • 可见性:锁的释放会将工作内存中的修改刷新到主内存
    • 可重入性:同一个线程可以多次获取同一个锁
  3. 使用方式

    • 同步实例方法:锁是当前实例对象
    public synchronized void method() {}
    
    • 同步静态方法:锁是当前类的Class对象
    public static synchronized void method() {}
    
    • 同步代码块:锁是指定对象
    synchronized(obj) {
        // 同步代码
    }
    
  4. volatile关键字

    • 保证变量的可见性:写操作立即刷新到主内存,读操作直接从主内存读取
    • 禁止指令重排序
    • 不保证原子性(适合单写多读场景)

3.5 单例与线程安全

答案:

  1. 饿汉式(线程安全)
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
  1. 双重检查锁定(DCL)
public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 静态内部类(推荐)
public class Singleton {
    private Singleton() {}
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
  1. 枚举单例(最佳实践)
public enum Singleton {
    INSTANCE;
    public void doSomething() {}
}

3.6 如何避免线程安全性问题

答案:

  1. 线程安全性问题成因
    • 多线程环境
    • 共享资源
    • 非原子性操作
    • 可见性问题
    • 指令重排序
  2. 避免方法
    • 不共享数据:
      • 使用ThreadLocal
      • 使用局部变量
    • 不可变对象:
      • 使用final修饰的不可变对象
    • 同步机制:
      • synchronized
      • volatile
      • 原子类
      • Lock
    • 线程安全容器:
      • ConcurrentHashMap
      • CopyOnWriteArrayList
    • 避免逃逸:
      • 不要将内部对象发布到外部

四、锁

4.1 锁的分类

答案:

  1. 自旋锁:线程不放弃CPU时间片,循环尝试获取锁(减少上下文切换)
  2. 阻塞锁:获取不到锁时线程进入阻塞状态
  3. 重入锁(ReentrantLock):同一线程可多次获取同一把锁
  4. 读写锁(ReadWriteLock):读锁共享,写锁互斥
  5. 互斥锁:同一时刻只有一个线程能持有锁(synchronized)
  6. 悲观锁:假定并发冲突严重,先加锁再操作(synchronized, ReentrantLock)
  7. 乐观锁:假定冲突少,先操作再检查(CAS, 版本号机制)
  8. 公平锁:按申请锁的顺序获取锁(ReentrantLock(true))
  9. 非公平锁:不保证顺序,可能插队(synchronized, ReentrantLock(false))
  10. 偏向锁:无竞争时消除同步开销
  11. 独占锁:同一时刻只有一个线程能持有锁
  12. 共享锁:多个线程可以同时持有锁(如读锁)
  13. 轻量级锁(Lightweight Lock):共享资源发生竞争的时候,CAS 操作成功,当前线程就获得了锁,并且锁的状态被标记为轻量级锁。
  14. 重量级锁(Heavyweight Lock):当轻量级锁膨胀失败时,锁会升级为重量级锁。重量级锁会使其他线程阻塞,而不是进行自旋等待,防止CPU空转浪费资源

4.2 Lock接口

答案:

  1. 使用方式
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock(); // 必须在finally中释放
}
  1. 与synchronized的区别
    • synchronized是关键字,Lock是接口
    • synchronized自动释放锁,Lock必须手动释放
    • Lock可以尝试获取锁(tryLock)、可中断(lockInterruptibly)、可定时
    • Lock可以实现公平锁
    • Lock提供Condition实现更灵活的线程通信
    • synchronized更简单,Lock更灵活
  2. 实现自定义锁
    需要实现Lock接口,通常基于AQS(AbstractQueuedSynchronizer)实现。
  3. AQS浅析
    AQS是构建锁和同步器的框架,核心包括:
    • 一个volatile int state表示同步状态
    • 一个FIFO线程等待队列
    • 通过CAS操作修改state
    • 提供acquire/release模板方法
  4. ReentrantLock公平锁实现
    公平锁在获取锁时检查等待队列是否有前驱节点:
final boolean fairSyncTryAcquire(int acquires) {
    if (getState() == 0) {
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 重入逻辑...
}
  1. 锁降级
    在持有写锁的情况下获取读锁,然后释放写锁的过程:
// 获取写锁
writeLock.lock();
try {
    // 修改数据...
    // 获取读锁(锁降级开始)
    readLock.lock();
} finally {
    writeLock.unlock(); // 释放写锁
}
// 此时只有读锁,完成降级
try {
    // 读取数据...
} finally {
    readLock.unlock();
}
  1. StampedLock
  • 三种模式:写锁、悲观读锁、乐观读
  • 乐观读不阻塞写操作
  • 适合读多写少场景
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 乐观读
if (!lock.validate(stamp)) { // 检查是否被修改
    stamp = lock.readLock(); // 转为悲观读
    try {
        // 读取数据...
    } finally {
        lock.unlockRead(stamp);
    }
}

五、线程间通信

5.1 wait、notify、notifyAll

答案:

  1. 基本用法
    • wait(): 释放锁并进入等待状态
    • notify(): 随机唤醒一个等待线程
    • notifyAll(): 唤醒所有等待线程
  2. 使用规范
synchronized(lock) {
    while(条件不满足) { // 必须用while而不是if
        lock.wait();
    }
    // 执行操作
    // 可选:lock.notifyAll();
}
  1. 注意事项:
    • 必须在同步块中调用
    • wait()会释放锁,notify()不会释放锁
    • 被唤醒的线程需要重新获取锁才能继续执行
    • 优先使用notifyAll()避免信号丢失

5.3 生产者消费者模式

答案:

经典实现:

class Buffer {
    private Queue<Integer> queue = new LinkedList<>();
    private int capacity;
    
    public Buffer(int capacity) {
        this.capacity = capacity;
    }
    
    public synchronized void produce(int item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();
        }
        queue.add(item);
        notifyAll();
    }
    
    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        int item = queue.remove();
        notifyAll();
        return item;
    }
}

使用BlockingQueue简化:

BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

// 生产者
queue.put(item);

// 消费者
int item = queue.take();

5.5 Thread.join()

答案:

  1. 作用
    • 让当前线程等待被调用join()的线程执行完毕
    • 常用于等待多个线程完成后再继续执行
  2. 使用
Thread t1 = new Thread(() -> {...});
t1.start();
t1.join(); // 主线程等待t1完成
  1. 原理
    底层调用wait()实现:
public final synchronized void join(long millis) {
    while (isAlive()) {
        wait(millis);
    }
}

5.6 ThreadLocal

答案:

  1. 简介
    • 线程局部变量,每个线程有自己独立的副本
    • 避免共享变量带来的线程安全问题
  2. 使用
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");
threadLocal.set("value");
String value = threadLocal.get();
threadLocal.remove(); // 重要:防止内存泄漏
  1. 原理
    • 每个Thread对象内部有ThreadLocalMap
    • ThreadLocal作为key,存储线程私有值
    • key是弱引用,value是强引用
  2. 内存泄漏问题
    • 原因:ThreadLocalMap的Entry中key是弱引用,value是强引用
    • 解决:使用完调用remove()清理
  3. InheritableThreadLocal
    • 子线程可以继承父线程的ThreadLocal值
    • 适用于需要传递上下文给子线程的场景

5.7 Condition

答案:

  1. 作用
    • 类似于wait/notify,但更灵活
    • 一个Lock可以创建多个Condition
  2. 使用
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 等待
lock.lock();
try {
    while (条件不满足) {
        condition.await();
    }
    // 处理
} finally {
    lock.unlock();
}

// 唤醒
lock.lock();
try {
    // 改变条件
    condition.signalAll();
} finally {
    lock.unlock();
}
  1. 优势:
    • 可以精确唤醒特定条件的线程
    • 支持中断等待
    • 支持超时等待

六、原子类

6.1 什么是原子类?

答案:
原子类是基于CAS(Compare And Swap)实现的线程安全类,位于java.util.concurrent.atomic包中。它们提供原子操作来更新基本类型、数组、引用等,无需使用锁。

特点:

  • 线程安全
  • 高性能(无锁或低锁)
  • 基于CAS实现
  • 适用于计数器、累加器等场景

6.2-6.5 原子类分类

答案:

  1. 基本类型
    • AtomicInteger
    • AtomicLong
    • AtomicBoolean
  2. 数组类型
    • AtomicIntegerArray
    • AtomicLongArray
    • AtomicReferenceArray
  3. 引用类型
    • AtomicReference
    • AtomicStampedReference(解决ABA问题)
    • AtomicMarkableReference
  4. 字段更新器
    • AtomicIntegerFieldUpdater
    • AtomicLongFieldUpdater
    • AtomicReferenceFieldUpdater

示例:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子自增
counter.compareAndSet(expect, update); // CAS操作

七、并发容器

7.1-7.3 同步容器与并发容器

答案:

  1. 同步容器
    • Collections.synchronizedXXX()包装的容器
    • 如:synchronizedList, synchronizedMap
    • 通过synchronized实现线程安全
    • 性能较差(全表锁)
  2. 并发容器
    • java.util.concurrent包中的容器
    • 实现:
      • ConcurrentHashMap:分段锁/CAS
      • CopyOnWriteArrayList:写时复制
      • ConcurrentLinkedQueue:CAS无锁队列
      • BlockingQueue:阻塞队列(Array/LinkedBlockingQueue)
    • 特点:
      • 高并发性能
      • 弱一致性迭代器
      • 更细粒度的锁或无锁

八、并发工具类

8.1-8.4 并发工具类

答案:

  1. CountDownLatch

    • 允许一个或多个线程等待其他线程完成
    • 一次性使用
    CountDownLatch latch = new CountDownLatch(3);
    // 工作线程
    latch.countDown();
    // 主线程
    latch.await();
    
  2. CyclicBarrier

    • 让一组线程互相等待到达屏障点
    • 可重复使用
    CyclicBarrier barrier = new CyclicBarrier(3, () -> {
        // 所有线程到达后执行
    });
    // 工作线程
    barrier.await();
    
  3. Semaphore

    • 控制同时访问资源的线程数
    Semaphore semaphore = new Semaphore(5);
    semaphore.acquire();
    // 访问资源
    semaphore.release();
    
  4. Exchanger

    • 两个线程交换数据的同步点
    Exchanger<String> exchanger = new Exchanger<>();
    // 线程A
    String dataB = exchanger.exchange(dataA);
    // 线程B
    String dataA = exchanger.exchange(dataB);
    

九、线程池

9.1-9.7 线程池

答案:

  1. 为什么使用线程池
    • 降低资源消耗(减少创建销毁开销)
    • 提高响应速度(任务到达时已有线程)
    • 提高线程可管理性(统一分配、监控)
  2. 创建线程池
    • 通过ThreadPoolExecutor构造函数
    • 通过Executors工厂方法(不推荐生产环境使用)
  3. 核心参数
    • corePoolSize:核心线程数
    • maximumPoolSize:最大线程数
    • keepAliveTime:空闲线程存活时间
    • workQueue:任务队列
    • threadFactory:线程工厂
    • handler:拒绝策略
  4. 执行流程
    1. 提交任务
    2. 核心线程未满 → 创建新线程执行
    3. 核心线程已满 → 放入队列
    4. 队列已满 → 创建非核心线程执行
    5. 达到最大线程数 → 执行拒绝策略
  5. 拒绝策略
    • AbortPolicy:抛出RejectedExecutionException(默认)
    • CallerRunsPolicy:由提交任务的线程执行
    • DiscardPolicy:直接丢弃
    • DiscardOldestPolicy:丢弃队列最前面的任务
  6. Executor框架
    • Executor:执行接口
    • ExecutorService:扩展接口(生命周期管理)
    • ScheduledExecutorService:定时任务
    • Executors:工厂类
  7. 使用建议
    • 根据任务类型配置线程池(CPU密集型、IO密集型)
    • 使用有界队列防止资源耗尽
    • 自定义线程命名便于排查问题
    • 监控线程池状态

十、JVM与并发

10.1-10.3 JVM内存模型与并发

答案:

  1. JVM内存模型(JMM)
    • 主内存:存储共享变量
    • 工作内存:每个线程私有的内存空间
    • 规则:
      • 线程对变量的所有操作都必须在工作内存中进行
      • 不同线程不能直接访问对方工作内存中的变量
      • 变量传递需要通过主内存完成
  2. happens-before原则
    定义操作间的偏序关系,保证前一个操作的结果对后一个操作可见:
    • 程序顺序规则
    • 锁规则
    • volatile规则
    • 线程启动规则
    • 线程终止规则
    • 中断规则
    • 终结器规则
    • 传递性
  3. 指令重排序
    • 编译器/处理器为了优化性能可能改变指令顺序
    • as-if-serial语义:单线程程序执行结果不变
    • 多线程环境下可能导致问题
    • 通过volatile、synchronized、final等防止重排序

十一、实战与总结

11.1-11.4 并发实战

答案:
并发编程实战要点:

  1. 数据同步接口
    • 设计幂等接口
    • 使用乐观锁处理并发更新
    • 考虑分布式锁(如Redis)解决跨JVM同步
  2. 中间表设计
    • 读写分离
    • 适当冗余减少关联查询
    • 考虑分库分表策略
  3. 生产者-消费者模式
    • 使用BlockingQueue简化实现
    • 控制生产/消费速度匹配
    • 考虑背压机制防止队列积压

12. 并发编程面试总结

答案:
高频面试题总结:

  1. 线程状态及转换
  2. synchronized原理及优化(偏向锁、轻量级锁、重量级锁)
  3. volatile原理(内存屏障、禁止重排序)
  4. CAS原理及ABA问题
  5. AQS原理及实现(ReentrantLock、CountDownLatch等)
  6. ConcurrentHashMap实现原理
  7. 线程池参数配置及工作流程
  8. ThreadLocal原理及内存泄漏问题
  9. 死锁条件及排查方法(jstack)
  10. happens-before原则

掌握这些核心知识点,结合实际项目经验,能够应对大多数Java并发编程相关的面试问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值