Java面试并发案例

wen java案例 3

本文目录导读:

Java面试并发案例

  1. 案例一:双重检查锁(DCL)与 volatile
  2. 案例二:死锁的构造与排查
  3. 案例三:生产者-消费者模式(3种实现)
  4. 案例四:线程池参数与拒绝策略
  5. 案例五:并发安全的计数器
  6. 案例六:CountDownLatch 与 CyclicBarrier
  7. 案例七:CAS 的 ABA 问题
  8. 案例八:读写锁与缓存优化
  9. 案例九:并发容器选择
  10. 案例十:volatile 的可见性问题
  11. 面试冲刺要点

下面为你整理一份Java并发面试的综合案例集,包含高频考点、典型场景、代码示例和面试官追问,这些案例覆盖了可见性、原子性、死锁、线程池、并发容器、AQS等核心知识点。


双重检查锁(DCL)与 volatile

场景描述

实现一个线程安全的单例模式,要求懒加载高性能

错误示范(面试陷阱)

public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new Singleton();    // 问题所在!
                }
            }
        }
        return instance;
    }
}

问题分析

instance = new Singleton() 在JVM中分三步执行:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存空间

由于指令重排序,步骤2和3可能互换,线程A执行了3但未执行2时,线程B看到instance不为null,直接返回未初始化完成的对象

正确实现

public class Singleton {
    // volatile 禁止指令重排序
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

面试追问

  • volatile的三个特性:可见性、有序性(禁止重排序)、不保证原子性
  • 还有其他单例实现方式吗?(枚举、静态内部类)
  • 为什么不直接用synchronized修饰getInstance?(性能,每次进入都加锁)

死锁的构造与排查

场景描述

两个线程各自持有一把锁,都在等待对方释放。

代码演示

public class DeadLockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
    public static void main(String[] args) {
        // 线程1:先拿A再拿B
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1获取锁A");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1获取锁B");
                }
            }
        }).start();
        // 线程2:先拿B再拿A(与线程1顺序相反)
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2获取锁B");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {
                    System.out.println("线程2获取锁A");
                }
            }
        }).start();
    }
}

排查方式(面试重点)

# 1. 使用jps查看Java进程
jps
# 2. 使用jstack打印线程快照
jstack <PID>
# 3. 输出结果中会明确显示死锁
Found one Java-level deadlock:
"Thread-1":
  waiting to lock monitor 0x000000001d3d3e40 (object 0x00000000d6203a60, a java.lang.Object),
  which is held by "Thread-0"

避免死锁的策略

  • 锁顺序:所有线程按相同顺序加锁
  • 超时等待:使用tryLock(long timeout, TimeUnit unit)
  • 粗粒度锁:合并多个锁为一个

生产者-消费者模式(3种实现)

场景描述

固定容量的缓冲区,多个生产者添加数据,多个消费者取出数据。

synchronized + wait/notify

public class BlockingQueueDemo {
    private final LinkedList<Integer> queue = new LinkedList<>();
    private final int capacity = 10;
    public synchronized void produce(int value) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();  // 队列满,等待
        }
        queue.add(value);
        notifyAll();  // 唤醒消费者
    }
    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // 队列空,等待
        }
        int value = queue.removeFirst();
        notifyAll();  // 唤醒生产者
        return value;
    }
}

ReentrantLock + Condition(推荐)

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final LinkedList<Integer> queue = new LinkedList<>();
    private final int capacity = 10;
    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();  // 等待不满
            }
            queue.add(value);
            notEmpty.signal();   // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();  // 等待不空
            }
            int value = queue.removeFirst();
            notFull.signal();     // 唤醒生产者
            return value;
        } finally {
            lock.unlock();
        }
    }
}

BlockingQueue(最简单)

public class BlockingQueueProducerConsumer {
    // 使用现成的阻塞队列
    private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
    public void produce(int value) throws InterruptedException {
        queue.put(value);  // 自动阻塞
    }
    public int consume() throws InterruptedException {
        return queue.take();  // 自动阻塞
    }
}

面试追问

  • 为什么用while不用if?(防止虚假唤醒)
  • Condition和synchronized的区别?(多路等待、可中断、可超时、公平性)
  • Java中还有哪些阻塞队列?(LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue等)

线程池参数与拒绝策略

场景描述

设计一个线程池处理异步任务,要求:

  • 核心线程5个
  • 最大线程10个
  • 任务队列容量为100
  • 超过容量时丢弃最老的任务

代码实现

public class ThreadPoolConfig {
    public static ExecutorService createPool() {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            5,                      // corePoolSize
            10,                     // maximumPoolSize
            60L, TimeUnit.SECONDS,  // 非核心线程空闲60秒销毁
            new ArrayBlockingQueue<>(100),  // 工作队列
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy()  // 丢弃最老任务
        );
        return pool;
    }
}

线程池工作流程(面试必问)

提交任务 → 判断核心线程是否都在执行?
   ├─ 否 → 创建核心线程执行
   └─ 是 → 判断队列是否满?
       ├─ 否 → 放入队列等待
       └─ 是 → 判断线程数是否达到最大?
           ├─ 否 → 创建非核心线程执行
           └─ 是 → 执行拒绝策略

4种拒绝策略

策略 说明
AbortPolicy 抛出RejectedExecutionException(默认)
CallerRunsPolicy 由提交任务的线程自己执行
DiscardPolicy 直接丢弃新任务
DiscardOldestPolicy 丢弃队列最老的任务,加入新任务

面试追问

  • 为什么不推荐Executors.newFixedThreadPool()?(队列无界,可能OOM)
  • 核心线程在什么情况下会被销毁?allowCoreThreadTimeOut(true)
  • 线程池大小如何设置?(CPU密集型=n+1,IO密集型=2n)

并发安全的计数器

场景描述

多个线程同时对同一个变量进行自增操作,要求最终结果准确。

AtomicLong(推荐)

public class AtomicCounter {
    private final AtomicLong count = new AtomicLong(0);
    public void increment() {
        count.incrementAndGet();
    }
    public long get() {
        return count.get();
    }
}

LongAdder(高并发优化)

public class LongAdderCounter {
    private final LongAdder count = new LongAdder();
    public void increment() {
        count.increment();
    }
    public long get() {
        return count.sum();
    }
}

synchronized(简单但性能低)

public class SynchronizedCounter {
    private long count = 0;
    public synchronized void increment() {
        count++;
    }
    public synchronized long get() {
        return count;
    }
}

性能对比与选择

  • 单线程/低竞争:synchronized 性能不错
  • 中等竞争:AtomicLong(CAS自旋)
  • 高并发写入:LongAdder(分段+最后汇总)

源码原理:CAS(比较并交换)基于Unsafe.compareAndSwapLong,硬件层面保证原子性。


CountDownLatch 与 CyclicBarrier

CountDownLatch(倒计时门闩)

需求:主线程等待所有子线程完成后再继续。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务");
                latch.countDown();  // 完成任务,计数减1
            }, "线程" + i).start();
        }
        latch.await();  // 等待计数器归零
        System.out.println("所有线程已执行完毕,主线程继续");
    }
}

CyclicBarrier(循环屏障)

需求:所有线程到达屏障后,同时继续执行。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(4, () -> {
            System.out.println("所有线程已到齐,出发!");
        });
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 到达屏障");
                try {
                    barrier.await();  // 等待其他线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 继续执行");
            }, "线程" + i).start();
        }
    }
}
特性 CountDownLatch CyclicBarrier
可重用
阻塞对象 调用await的线程 调用await的线程
计数方式 递减(不可重置) 递减后自动重置
典型场景 等待任务完成 线程间互相等待

CAS 的 ABA 问题

场景描述

CAS操作中,一个值从A变为B再变回A,CAS会认为它没变过,但实际上发生了变化。

代码演示

public class ABADemo {
    private static AtomicReference<Integer> ref = new AtomicReference<>(100);
    public static void main(String[] args) {
        // 线程1:将100改为101,再改回100
        new Thread(() -> {
            ref.compareAndSet(100, 101);
            ref.compareAndSet(101, 100);
            System.out.println("线程1执行了ABA操作");
        }).start();
        // 此时线程2可能感知不到变化
    }
}

解决方案:AtomicStampedReference(带版本号)

public class AtomicStampedDemo {
    private static AtomicStampedReference<Integer> ref = 
        new AtomicStampedReference<>(100, 0);  // 初始值100,版本号0
    public static void main(String[] args) {
        int[] stamp = new int[1];
        Integer value = ref.get(stamp);
        int currentStamp = stamp[0];
        // 执行CAS时,需要同时匹配值和版本号
        boolean success = ref.compareAndSet(
            value, 200,          // 期望值和新值
            currentStamp, currentStamp + 1  // 期望版本号和新版本号
        );
        System.out.println("操作结果: " + success);
    }
}

面试追问

  • ABA问题在实际中会造成什么影响?(如链表重入环)
  • AtomicMarkableReference(只关心是否被修改过)和AtomicStampedReference(关心修改次数)的区别

读写锁与缓存优化

场景描述

实现一个线程安全的缓存,要求读操作可以并发,写操作独占。

代码实现

public class CacheDemo {
    private final Map<String, Object> cache = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    // 读操作:多个线程可以同时读
    public Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }
    // 写操作:独占
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

锁降级(高级考点)

public Object getWithUpgrade(String key) {
    readLock.lock();
    try {
        Object value = cache.get(key);
        if (value == null) {
            // 注意:不能在持有读锁时直接升级为写锁(会死锁)
            // 必须先释放读锁,再获取写锁
            readLock.unlock();
            writeLock.lock();
            try {
                // 双重检查
                value = cache.get(key);
                if (value == null) {
                    value = "compute" + key;
                    cache.put(key, value);
                }
            } finally {
                writeLock.unlock();
            }
            readLock.lock();
        }
        return value;
    } finally {
        readLock.unlock();
    }
}

面试追问

  • 读写锁的公平性问题
  • StampedLock的乐观读(比读写锁更快的无锁读)

并发容器选择

场景对比

public class ConcurrentContainerDemo {
    // 1. 线程安全的Map
    ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
    // 2. 线程安全的List
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    // 或者
    List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
    // 3. 线程安全的Set
    ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<>();
    // 或者
    CopyOnWriteArraySet<String> copyOnWriteSet = new CopyOnWriteArraySet<>();
    // 4. 队列
    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    // 5. 阻塞队列
    BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
}

核心原理(面试高频)

  • ConcurrentHashMap:JDK 8 + 使用CAS + synchronized + 红黑树(链表长度>=8转树)
  • CopyOnWriteArrayList:写操作先复制新数组,修改后替换 volatile 数组引用,适合读多写少
  • ConcurrentLinkedQueue:无界并发队列,CAS实现入队/出队

volatile 的可见性问题

场景描述(经典面试题)

public class VolatileDemo {
    private static boolean flag = true;  // 不加volatile
    // private static volatile boolean flag = true;  // 加volatile
    public static void main(String[] args) {
        new Thread(() -> {
            while (flag) {
                // 这里什么都不做
            }
            System.out.println("线程1退出");
        }).start();
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        flag = false;  // 主线程修改flag
        System.out.println("主线程修改flag为false");
    }
}

现象分析

  • 不加volatile:线程1可能永远不退出(会读取到旧值)
  • 加volatile:线程1能立即感知到变化,退出循环

深挖原理

  • JMM(Java内存模型):线程有工作内存(栈/缓存),主内存共享
  • volatile 通过 Lock 前缀指令实现缓存一致性协议(MESI)
  • 写操作立即刷新到主内存,并使其他线程的缓存行失效

注意

volatile 不保证原子性,count++ 这种操作仍需要锁或原子类。


面试冲刺要点

考点 核心关注点
线程状态 NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
锁机制 synchronized(偏向锁/轻量级锁/重量级锁升级)vs ReentrantLock(公平/非公平)
AQS原理 状态state、CLH队列、独占/共享模式
ThreadLocal 线程隔离、内存泄漏(ThreadLocalMap的弱引用key)、应用场景
异步编程 CompletableFuture的组合、异常处理
并发工具类 Semaphore、Exchanger、Phaser

  1. 先答结论,再展开原理:面试官通常先看你的回答是否准确
  2. 结合代码和场景:每个结论都准备一个例子
  3. 主动说明权衡:比如悲观锁 vs 乐观锁、原子类 vs 锁的性能对比
  4. 展示排查能力:死锁用jstack、CPU飙升用jstack查看线程状态、OOM用jmap分析堆

希望这份并发案例能帮助你在面试中系统地展示自己的并发编程能力!

抱歉,评论功能暂时关闭!