Featured image of post Java 面试合集:并发编程与 JUC 实战

Java 面试合集:并发编程与 JUC 实战

Java 并发编程面试合集:线程安全方案、synchronized 与 ReentrantLock、ThreadLocal 实战、Future 与 Callable、ConcurrentHashMap 原理、AQS 队列同步器

本文写于 2015 年 6 月——JDK 8 刚普及,Lambda 表达式 + Stream API 改变 Java 并发编程范式。

一、如何保证线程安全

1.1 多线程安全定义

多线程安全是指在并发环境下,多个线程访问共享资源时,程序能够正确地执行,而不会出现数据不一致或竞争条件等问题。反之,如果程序出现了数据不一致、死锁、饥饿等问题,就称为线程不安全。

1.2 6 大线程安全方案

方案适用场景示例
synchronized单体应用同步synchronized(lock) { ... }
ReentrantLock需要可中断/公平锁lock.lock(); try { ... } finally { lock.unlock(); }
Atomic简单变量原子操作AtomicInteger count = new AtomicInteger()
线程安全容器集合操作ConcurrentHashMapCopyOnWriteArrayList
ThreadLocal每线程独立变量ThreadLocal<User> currentUser
volatile状态标志可见性volatile boolean running = true

1.3 选型决策

二、面试场景:JSON 字段并发修改

2.1 场景描述

有一个 key 对应的 value 是一个 json 结构,json 当中有好几个子任务,这些子任务如果对 key 进行修改的话,会不会存在线程安全的问题?如何解决?如果是多个节点的情况,应该怎么加锁?

2.2 单节点方案:ReentrantLock

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class KeyManager {
    private final ReentrantLock lock = new ReentrantLock();
    private String key = "{\"tasks\": [\"task1\", \"task2\"]}";

    public String readKey() {
        lock.lock();
        try {
            return key;
        } finally {
            lock.unlock();
        }
    }

    public void updateKey(String newKey) {
        lock.lock();
        try {
            this.key = newKey;
        } finally {
            lock.unlock();
        }
    }
}

2.3 多节点方案:Redisson 分布式锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class DistributedKeyManager {
    private final RedissonClient redisson;

    public DistributedKeyManager() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        this.redisson = Redisson.create(config);
    }

    public void updateKey(String key, String newValue) {
        RLock lock = redisson.getLock(key);
        lock.lock();
        try {
            // 模拟读取和更新操作
            String currentValue = readFromDatabase(key);
            String updatedValue = modifyJson(currentValue, newValue);
            writeToDatabase(key, updatedValue);
        } finally {
            lock.unlock();
        }
    }

    private String readFromDatabase(String key) {
        return "{\"tasks\": [\"task1\", \"task2\"]}";
    }

    private String modifyJson(String json, String newValue) {
        return json.replace("task1", newValue);
    }

    private void writeToDatabase(String key, String value) {
        // 写回 DB
    }
}

为什么用 Redisson

  • 内置看门狗自动续期(防止业务执行慢导致锁过期)
  • 支持公平锁/可重入锁/读写锁
  • Redlock 算法实现多节点安全

三、ThreadLocal 实战

3.1 什么是 ThreadLocal

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全。

3.2 典型应用:SimpleDateFormat

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 错误:SimpleDateFormat 是非线程安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse() 可能 NumberFormatException

// 正确:使用 ThreadLocal
private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public Date parse(String dateStr) {
    return sdfThreadLocal.get().parse(dateStr);
}

生动比喻

比如饭店要做一道菜,但是有 5 个厨师一起做,这样的话就很乱了,因为如果一个厨师已经放过盐了,假如其他厨师都不知道,于是都各自放了一次盐,导致最后的菜很咸。这就好比多线程的情况,线程不安全。我们用了 ThreadLocal 之后,相当于每个厨师只负责自己的一道菜,一共有 5 道菜,这样的话就非常清晰明了,不会出现问题。

3.3 ThreadLocal 内存泄漏

Q:ThreadLocal 为什么会导致内存泄漏?

原理

  • ThreadLocalMap 的 Entry 继承 WeakReference<ThreadLocal<?>>
  • 当 ThreadLocal 失去强引用后,Entry 的 key 被 GC 回收
  • 但 value 仍然被 Thread → ThreadLocalMap → Entry 强引用
  • 线程不结束(如线程池核心线程)→ value 永远不释放

解决

1
2
3
4
5
6
try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove();  // 必须 remove
}

四、Future 与 Callable

4.1 为什么需要 Future

线程执行完要有返回值的,使用 Callable 接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ExecutorService executor = Executors.newFixedThreadPool(5);

// Runnable 没有返回值
Runnable runnable = () -> System.out.println("hello");

// Callable 有返回值 + 抛异常
Callable<Integer> callable = () -> {
    Thread.sleep(1000);
    return 42;
};

Future<Integer> future = executor.submit(callable);
Integer result = future.get();  // 阻塞等结果

4.2 Future 的局限性

问题描述
阻塞get() 阻塞,无法链式
异常异常被包装在 ExecutionException
多个 Future需要手动编排

4.3 解决方案:CompletableFuture(JDK 8+)

1
2
3
4
5
6
7
CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> {
        // 异步任务
        return 42;
    })
    .thenApply(v -> v * 2)  // 链式处理
    .thenAccept(System.out::println);  // 最终消费

五、ConcurrentHashMap 原理

5.1 JDK 7 vs JDK 8 实现

维度JDK 7JDK 8
数据结构Segment + HashEntry 数组Node 数组 + 链表 + 红黑树
锁粒度Segment(16 个)桶(每个头节点)
锁类型ReentrantLockCAS + synchronized
不加锁不加锁
锁 Segment锁桶头节点

5.2 JDK 8 关键优化

  1. CAS + synchronized:锁粒度更细
  2. 红黑树化:链表长度 > 8 时转红黑树
  3. volatile + 协调:保证内存可见性
  4. 扩容协助:多线程协同扩容

5.3 size() 实现

1
2
3
// JDK 8 通过 baseCount + CounterCell[] 数组实现
// 每个线程更新时用 CAS 累加到自己的 CounterCell
// 总数 = baseCount + sum(CounterCell.values)

六、AQS(AbstractQueuedSynchronizer)

6.1 AQS 是什么

AQS 是 JUC(java.util.concurrent)包的核心,用于构建锁和同步器。

6.2 核心思想

1
AQS = state 状态 + CLH 等待队列 + 模板方法
  • state:同步状态(0 表示未占用,1 表示已占用)
  • CLH 队列:等待线程的 FIFO 队列
  • 模板方法tryAcquire / tryRelease / tryAcquireShared / tryReleaseShared

6.3 基于 AQS 的同步器

同步器类型用途
ReentrantLock独占可重入互斥锁
ReentrantReadWriteLock独占+共享读写锁
Semaphore共享信号量
CountDownLatch共享倒计时门闩
CyclicBarrier共享循环屏障
ThreadPool Executor共享线程池

七、synchronized vs ReentrantLock

7.1 对比

维度synchronizedReentrantLock
实现JVM 内置(monitor)JDK 代码(基于 AQS)
锁升级无锁 → 偏向锁 → 轻量锁 → 重量锁直接 CAS + LockSupport
中断不可中断lockInterruptibly()
公平性非公平可选公平/非公平
条件变量wait/notify(1 个)Condition(多个)
性能JDK 6+ 优化后接近略低
锁粒度对象/类显式 lock/unlock

7.2 synchronized 锁升级(JDK 6+ 优化)

状态描述适用
无锁无线程竞争初始状态
偏向锁单线程反复进入(CAS 替换 ThreadID)始终单线程
轻量锁短时间竞争(CAS 自旋)低竞争
重量锁操作系统互斥量高竞争

八、写在最后

并发编程面试要点

  1. 基础概念:6 大线程安全方案选型
  2. 原理:ConcurrentHashMap 实现、AQS 原理、synchronized 锁升级
  3. 实战:ThreadLocal 内存泄漏、Future vs CompletableFuture
  4. 进阶:多节点分布式锁、Reactor 模式、协程(Project Loom)

参考资料

使用 Hugo 构建
主题 StackJimmy 设计