Java 多线程基础(3)
作者:没有四次元口袋的蓝胖
日期:2026-06-26
标签:Java, 多线程
一、并发集合
1.1 ConcurrentHashMap ⭐⭐⭐
线程安全的 HashMap,面试必问。
JDK 1.7 实现:Segment 分段锁
- 把数据分成16个段(Segment),每个段有自己的 ReentrantLock
- 不同段可以并发访问
- 并发度默认16
JDK 1.8 实现:CAS + synchronized
- 放弃分段锁,使用 Node 数组 + CAS + synchronized
- 锁粒度更细:只锁当前桶(桶为空用 CAS 插入,非空用 synchronized)
- 引入红黑树优化查询(链表长度 > 8 转红黑树)
java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.get("key");
// 原子操作(线程安全)
map.putIfAbsent("key", 1); // 不存在才放入
map.computeIfAbsent("key", k -> 1); // 不存在才计算并放入(推荐)
ConcurrentHashMap 不允许 key 或 value 为 null! 这是设计决策,避免二义性(null 代表不存在还是值就是 null?)。
1.2 CopyOnWriteArrayList
写时复制的线程安全 List:
java
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
原理:
- 读操作不加锁,直接读原数组
- 写操作(add/set/remove)加锁,复制一份新数组,修改新数组,再替换引用
- 读写不互斥,写写互斥
适用场景: 读多写少(如白名单、配置列表、监听器列表)
缺点:
- 写操作要复制整个数组,内存开销大
- 数据一致性是最终一致,不是实时的
- 不适合写频繁的场景
1.3 ConcurrentLinkedQueue
线程安全的无界队列(非阻塞):
java
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("A");
queue.offer("B");
String item = queue.poll(); // 取出并移除
使用 CAS 实现,不需要加锁,性能高。适合生产者-消费者场景但不需要阻塞等待时。
1.4 BlockingQueue(阻塞队列)
线程池的核心组件,也是生产者-消费者模式的关键:
| 实现类 | 特点 |
|---|---|
| ArrayBlockingQueue | 有界数组队列,必须指定容量 |
| LinkedBlockingQueue | 可选有界链表队列,默认 Integer.MAX_VALUE |
| PriorityBlockingQueue | 无界优先级队列 |
| DelayQueue | 延迟队列,元素到期才能取出 |
| SynchronousQueue | 不存储元素,直接手递手传递 |
java
// 常用操作
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("A"); // 满了则阻塞
queue.take(); // 空了则阻塞
queue.offer("B", 3, TimeUnit.SECONDS); // 超时尝试
queue.poll(3, TimeUnit.SECONDS); // 超时尝试
SynchronousQueue 的特殊性:
java
SynchronousQueue<String> sq = new SynchronousQueue<>();
// put 会阻塞,直到另一个线程调用 take
// 没有容量,每个 put 必须等待一个 take,反之亦然
// 适合"手递手"传递场景(如 CachedThreadPool 就用它)
1.5 并发集合对比
| 集合 | 线程安全 | 底层实现 | 特点 |
|---|---|---|---|
| ConcurrentHashMap | ✅ | CAS + synchronized | 高性能,不允许 null |
| Hashtable | ✅ | synchronized 全表锁 | 性能差,不允许 null |
| Collections.synchronizedMap | ✅ | 包装 + 全表锁 | 性能差 |
| CopyOnWriteArrayList | ✅ | 写时复制 | 读多写少场景 |
| ConcurrentLinkedQueue | ✅ | CAS 无锁 | 非阻塞队列 |
📌 常见坑点与面试题
Q:ConcurrentHashMap 为什么不允许 null?
并发环境下,get 返回 null 无法区分是"key 不存在"还是"value 是 null"。HashMap 允许 null 是因为单线程可以自行判断,并发场景不行。
Q:ConcurrentHashMap 在 JDK 1.7 和 1.8 的区别?
- 1.7:Segment 分段锁(ReentrantLock),并发度默认16
- 1.8:Node 数组 + CAS + synchronized,锁粒度细化到单个桶,引入红黑树优化长链表(>8转树)
Q:CopyOnWriteArrayList 的缺点是什么?
- 写操作复制整个数组,内存占用翻倍
- 写的数据一致性是最终一致,不是实时的
- 只适合读多写少的场景
二、原子类(Atomic)
2.1 为什么需要原子类?
synchronized 虽然能保证原子性,但开销大(涉及线程阻塞、唤醒)。原子类利用 CAS(Compare-And-Swap)实现无锁的线程安全操作,性能更高。
2.2 CAS 原理
CAS(内存值V, 预期值A, 新值B)
→ 如果 V == A,则 V = B,返回 true
→ 如果 V != A,说明被别人改了,返回 false(不修改,可以重试)
特点: 比较并交换,一条 CPU 指令完成(cmpxchg),不需要加锁。
CAS 的三大问题:
- ABA 问题:值从 A → B → A,CAS 认为没变过
- 自旋开销:长时间失败会一直循环,消耗 CPU
- 只能保证一个变量的原子操作:多个变量需要用锁或 AtomicReference
2.3 常用原子类
java
// 1. AtomicInteger ------ 原子整数
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++count,返回新值
count.getAndIncrement(); // count++,返回旧值
count.addAndGet(5); // count += 5
count.decrementAndGet(); // --count
int val = count.get(); // 获取值
// 2. AtomicLong ------ 原子长整型
AtomicLong id = new AtomicLong(0);
id.incrementAndGet();
// 3. AtomicBoolean ------ 原子布尔
AtomicBoolean flag = new AtomicBoolean(false);
flag.compareAndSet(false, true); // CAS 操作
// 4. AtomicReference ------ 原子引用
AtomicReference<User> ref = new AtomicReference<>(new User("张三"));
ref.compareAndSet(oldUser, newUser);
2.4 原子数组
java
AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.getAndIncrement(0); // arr[0]++
arr.compareAndSet(1, 5, 10); // arr[1] == 5 则改为 10
2.5 解决 ABA 问题
java
// AtomicStampedReference:带版本号的原子引用
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
int stamp = ref.getStamp(); // 获取版本号
// CAS 时同时比较值和版本号
boolean success = ref.compareAndSet(1, 2, stamp, stamp + 1);
原理: 每次修改版本号+1,即使值从 A→B→A,版本号已经变了,CAS 能检测出来。
2.6 LongAdder(高并发计数器)
java
LongAdder adder = new LongAdder();
adder.increment();
adder.add(10);
long sum = adder.sum(); // 获取总和
比 AtomicLong 性能好的原因:
- 内部将一个值分散到多个 Cell 中,不同线程写不同的 Cell,减少竞争
- 最终 sum() 时汇总所有 Cell
- 牺牲空间换时间,适合高并发统计场景
📌 常见坑点与面试题
Q:CAS 是什么?有什么问题?
CAS = Compare And Swap,比较并交换,是一种无锁算法。问题:
- ABA 问题(用 AtomicStampedReference 解决)
- 自旋时间长,CPU 开销大
- 只能保证一个变量的原子操作
Q:AtomicInteger 的 incrementAndGet 是线程安全的吗?
是的。底层用 CAS + 自旋实现,不需要加锁,比 synchronized 性能更好。
Q:LongAdder 和 AtomicLong 怎么选?
- 低并发:AtomicLong 够用
- 高并发统计场景:LongAdder 更好(分散竞争,减少 CAS 冲突)
三、线程局部变量(ThreadLocal)
3.1 什么是 ThreadLocal?
为每个线程提供独立的变量副本,各线程之间互不影响。
java
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 线程A
threadLocal.set("线程A的数据");
System.out.println(threadLocal.get()); // "线程A的数据"
// 线程B
threadLocal.set("线程B的数据");
System.out.println(threadLocal.get()); // "线程B的数据"
3.2 常用方法
java
// 设置值
threadLocal.set(value);
// 获取值
Object val = threadLocal.get();
// 删除值(⚠️ 必须手动调用!)
threadLocal.remove();
// 带初始值(方式一:匿名内部类)
ThreadLocal<List<String>> listHolder = new ThreadLocal<List<String>>() {
@Override
protected List<String> initialValue() {
return new ArrayList<>();
}
};
// 带初始值(方式二:Java 8+ Lambda)
ThreadLocal<SimpleDateFormat> sdfHolder = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
3.3 典型应用场景
场景1:保存用户登录信息(Web 开发最常用)
java
// 过滤器中设置
public class LoginFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
String token = req.getHeader("token");
User user = parseToken(token);
UserContext.set(user); // 存入 ThreadLocal
try {
chain.doFilter(req, resp);
} finally {
UserContext.remove(); // ⚠️ 请求结束必须清除!
}
}
}
// 业务代码中直接获取
public class OrderService {
public void createOrder() {
User currentUser = UserContext.get(); // 直接拿到当前用户
// 不需要层层传递 user 参数
}
}
// ThreadLocal 工具类
public class UserContext {
private static final ThreadLocal<User> HOLDER = new ThreadLocal<>();
public static void set(User user) { HOLDER.set(user); }
public static User get() { return HOLDER.get(); }
public static void remove() { HOLDER.remove(); }
}
场景2:SimpleDateFormat 线程安全问题
java
// SimpleDateFormat 不是线程安全的!
// 用 ThreadLocal 给每个线程一个副本
private static final ThreadLocal<SimpleDateFormat> SDF = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
// 使用时
String formatted = SDF.get().format(new Date());
场景3:数据库连接 / Session 管理
java
// 每个线程持有自己的数据库连接
private static final ThreadLocal<Connection> connHolder = new ThreadLocal<>();
3.4 原理简述
Thread 对象
└── ThreadLocalMap(Thread 内部类)
├── key: ThreadLocal 对象的弱引用
└── value: 你 set 进去的值(强引用)
每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象(弱引用),value 是你存入的值。调用 threadLocal.set(value) 时,实际是把值存到当前线程的 map 里。
3.5 内存泄漏问题 ⭐⭐⭐
问题: ThreadLocalMap 的 key 是 ThreadLocal 的弱引用 。如果 ThreadLocal 对象被 GC 回收,key 变成 null,但 value 还被 ThreadLocalMap 强引用着,导致 value 无法回收 → 内存泄漏。
正常情况:ThreadLocal(强引用) → key(弱引用) → value
GC后: ThreadLocal 被回收 → key=null → value 还在!
解决方案:用完必须调用 remove()!
java
try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove(); // ⚠️ 必须清理!
}
在线程池中尤其危险: 线程被复用,如果不清理,上一个任务的值会"泄漏"到下一个任务,导致数据错乱。
3.6 InheritableThreadLocal
父线程的值可以传递给子线程:
java
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("父线程的值");
new Thread(() -> {
System.out.println(itl.get()); // "父线程的值"
}).start();
但在线程池中不好用(线程是预先创建好的,不是 new 出来的),推荐用 TransmittableThreadLocal(阿里开源 Ttl 框架)。
📌 常见坑点与面试题
Q:ThreadLocal 为什么会内存泄漏?怎么避免?
ThreadLocalMap 的 key 是弱引用,ThreadLocal 被回收后 key 为 null,但 value 还被强引用。如果线程不销毁(如线程池),value 就一直存在。解决方案:每次用完在 finally 中调用 remove()。
Q:ThreadLocal 和 synchronized 的区别?
- ThreadLocal:通过"空间换时间",每个线程一份副本,互不干扰
- synchronized:通过"时间换空间",加锁让线程排队访问
- 两者目的不同:ThreadLocal 解决数据隔离 ,synchronized 解决数据共享的安全访问
Q:SimpleDateFormat 为什么不是线程安全的?
内部有共享的 Calendar 字段,多个线程同时调用 format() 会互相修改 Calendar 的值。解决方案:用 ThreadLocal 给每个线程一个副本,或者用 Java 8 的 DateTimeFormatter(线程安全)。
Q:线程池中用 ThreadLocal 要注意什么?
线程池中的线程是复用的,任务完成后必须 remove(),否则上一个任务的数据会泄漏到下一个任务。
四、思维导图速览
并发工具
├── 并发集合
│ ├── ConcurrentHashMap
│ │ ├── JDK 1.7:Segment 分段锁
│ │ └── JDK 1.8:CAS + synchronized + 红黑树
│ ├── CopyOnWriteArrayList(写时复制,读多写少)
│ ├── ConcurrentLinkedQueue(CAS 无锁队列)
│ └── BlockingQueue
│ ├── ArrayBlockingQueue(有界)
│ ├── LinkedBlockingQueue(可选有界)
│ └── SynchronousQueue(不存储,手递手)
├── 原子类
│ ├── CAS 原理(Compare-And-Swap)
│ ├── AtomicInteger / AtomicLong / AtomicBoolean
│ ├── ABA 问题 → AtomicStampedReference
│ └── LongAdder(高并发计数,分散竞争)
└── ThreadLocal
├── 每个线程独立副本
├── 典型场景:用户信息、SimpleDateFormat、数据库连接
├── 原理:Thread → ThreadLocalMap → (key弱引用, value强引用)
├── 内存泄漏 → 必须 remove()
└── InheritableThreadLocal(父子传递)
五、写在最后
- 并发集合:ConcurrentHashMap 的 1.7 vs 1.8 实现区别是高频题,CopyOnWriteArrayList 的写时复制思想要理解
- 原子类:理解 CAS 原理和 ABA 问题是关键,LongAdder 的分散竞争思想值得了解
- ThreadLocal:内存泄漏的原因和解决方案是必考项,实际开发中保存用户信息是最常见的应用场景
- 面试高频排序:ConcurrentHashMap 原理 > ThreadLocal 内存泄漏 > CAS/ABA > CopyOnWriteArrayList 原理