并发设计模式
0. 总体
常见的8种简单并发设计模式中,可以分为:
- 避免共享变量
不可变模式、写时复制模式、线程本地存储模式 - 多线程版本if
保护性暂挂模式、放弃模式 - 简单分工
Thread-Per-Message模式、工作线程模式、生产者消费者模式
1. 不可变模式
1.1 定义
- 对象不可变,如String及基础包装类Integer/Long等。
- 如果需要修改,那么就创建一个新的不可变对象,而可变对象往往是修改自己的属性。比如String类的replace()方法修改的时候,并没有更改原字符串里面 value[]数组的内容,而是创建了一个新字符串。
1.2 享元模式
Boolean、Character、Byte、Short、Integer、Long、String等都用了享元模式。
比如Integer的valueOf(int i)方法,若是i的范围在[-128~127],那么会直接返回缓存的对应Integer实例对象。
基础类型包装类及String类等不适合作为锁的原因也是因为享元模式,看上去可能私有的锁,其实却是同一个实例。
2. 写时复制模式
需要修改的时候,不更新原有的实例数据,而是创建一个新的实例。比如刚刚提到的String类的replace()方法。
特点:
- 延时复制:只有在真正需要复制的时候才复制,而不是提前复制好
- 按需复制:可以只复制修改点。比如redis的bgsave过程,父进程只写时复制备份时修改的数据并重新指向新地址。
- 函数式编程应用广泛:函数式编程的基础是不可变性,所以修改操作需要写时复制模式来解决
如CopyOnWriteArraySet类、CopyOnWriteArrayList类。
3. 线程本地存储模式
Java最具有代表性的便是ThreadLocal类。
3.1 原理
3.1.1 底层实现
Thread实例持有ThreadLocalMap(其实是数组Entry[] table),key为ThreadLocal实例(数组索引计算规则为key.threadLocalHashCode & (table.length - 1)
),值为本地存储的泛型值。
class Thread {
//内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal<T>{
public T get() {
//首先获取线程持有的ThreadLocalMap
ThreadLocalMap map = Thread.currentThread().threadLocals;
//在ThreadLocalMap中查找变量
Entry e = map.getEntry(this);
return e.value;
}
static class ThreadLocalMap{
//内部是数组而不是Map
Entry[] table;
//根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key){
//省略查找逻辑
}
}
//Entry定义
static class Entry extends WeakReference<ThreadLocal>{
Object value;
}
}
3.1.2 设计原因
为何是Thread实例持有ThreadLocalMap,而不是ThreadLocalMap实例持有ThreadMap
- 若是ThreadLocalMap实例持有ThreadMap,那么由于ThreadLocal作为中间类明显生命周期更长,作为key的这些线程也不会消亡,会有内存泄漏问题。
- 每个Thread保存各自的键值对信息,也避免了有一个外部中间类掌握所有数据,数据封装更合理。
- Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。
弱引用: 当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
3.1.3 内存泄漏原因
内存泄露:程序在申请内存后,无法释放已申请的内存空间。
-
原因
- 线程池场景
在使用线程池时,ThreadLocal设置的值可能会一直保留在线程中,而线程会被线程池重复使用。
ThreadLocal由于是弱引用,会在方法调用结束后被回收置为Null,Entry情况如{Null:val}。但是由于val是强引用,且线程不会结束,故不会被回收。
如果在线程执行结束后没有显示清理ThreadLocal(除了remove外,其实get/set也会清理Entry中key为Null的情况),线程池中的线程引用的 ThreadLocal的值可能一直存在,导致内存泄漏。 - 长时间持有
如果在某些场景下,ThreadLocal 中的值被长时间持有,并且没有及时清理,就会导致 ThreadLocal 对应的对象无法被垃圾回收。
- 线程池场景
-
处理措施
- 适时清理
在使用完 ThreadLocal 存储的值后,尽量调用 remove 方法将其清除。可以使用 ThreadLocal.remove() 或者使用 try-with-resources 语句来确保资源的及时释放。
或者try (ThreadLocal tl = new ThreadLocal()) { // 使用 ThreadLocal }
ThreadLocal tl = new ThreadLocal(); try { // 使用 ThreadLocal } finally { tl.remove(); }
- 使用withInitial时注意使用静态方法
如果定义 ThreadLocal 时使用了 withInitial 方法,确保提供的初始化方法是静态的,以避免持有对外部类实例的引用。ThreadLocal<Integer> tl = ThreadLocal.withInitial(Count::get); // 调用Count类的静态方法get获取值
- 适时清理
更多内存泄露细节可见ThreadLocal的内存泄露。
3. 2 例子
看一段代码,利用ThreadLocal给线程池中不同线程生为线程号。
class ThreadId {
private static final AtomicLong atomicLong = new AtomicLong(1);
private static final ThreadLocal<Long> threadLocals = ThreadLocal.withInitial(atomicLong::getAndIncrement);
public static long get() {
return threadLocals.get();
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread() + " " + ThreadId.get());
});
}
executorService.shutdown();
}
}
打印如下,无论怎么执行,每个线程之间的ThreadId.get()都是不同的,但是自身的值又是不变的。
Thread[pool-1-thread-2,5,main] 3
Thread[pool-1-thread-1,5,main] 1
Thread[pool-1-thread-2,5,main] 3
Thread[pool-1-thread-3,5,main] 2
Thread[pool-1-thread-2,5,main] 3
Thread[pool-1-thread-1,5,main] 1
Thread[pool-1-thread-2,5,main] 3
Thread[pool-1-thread-2,5,main] 3
Thread[pool-1-thread-3,5,main] 2
Thread[pool-1-thread-1,5,main] 1
4. 保护性暂挂模式(Guarded Suspension)
4.1 定义及原理
一个线程等待某个条件成立,而另一个线程负责改变这个条件。当条件改变后,等待的线程将收到通知并继续执行。
Guarded Suspension 模式的结构图:
- 一个对象
GuardedObject,内部有一个受保护的对象的成员变量 - 两个成员方法
get(Predicate p)和onChanged(T obj)方法。
经典实现如下。
class GuardedObject<T> {
final Lock lock = new ReentrantLock();
final Condition done = lock.newCondition();
//受保护的对象
T obj;
//获取受保护对象
T get(Predicate<T> p) {
lock.lock();
try {
//MESA管程推荐写法
while (!p.test(obj)) {
done.await();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
//返回非空的受保护对象
return obj;
}
//事件通知方法
void onChanged(T obj) {
lock.lock();
try {
this.obj = obj;
done.signalAll();
} finally {
lock.unlock();
}
}
}
使用例子如下。
public class Test {
public static void main(String[] args) {
GuardedObject<Integer> guardedObject = new GuardedObject<>();
Thread thread = new Thread(() -> {
sleep(3);
guardedObject.onChanged(1);
});
thread.start();
System.out.println(guardedObject.get(t -> t!=null&&t > 0));
}
@SneakyThrows
public static void sleep(int i) {
TimeUnit.SECONDS.sleep(i);
}
}
打印如下, 可以看到3秒后由于thread修改了受保护对象的值为1,guardedObject.get(t -> t!=null&&t > 0)
验证通过而返回。
22:31:00.336 await
22:31:01.345 await
22:31:02.358 await
1
其实juc包中的Future接口就是保护性暂停模式的应用。
4.2 异步等待消息的例子
服务A和服务B间通过MQ交互,其中服务A的发送消息和接收消息的线程归属不同的线程,那么发送消息怎样同步等待消息返回呢?(类似RPC接口等待消息返回。因为若是走tcp传递消息,由于tcp是异步的那么调用也是异步的,等待是实现了异步转同步)
首先由于是不同线程,为了能匹配上,需要利用一个唯一的标识。这里可以利用消息的唯一message id实现,作为Map的key,GuardedObject则作为Map的value保存。
需要针对原始的GuardedObject进行功能扩展支持以上功能。
这里选择编写一个内部静态代理类管理key和GuardedObject的映射关系。
class GuardedObject<T> {
// 为了使用条件等待,创建锁
private final Lock lock = new ReentrantLock();
// 完成的条件变量
private final Condition done = lock.newCondition();
// 受保护的对象
private T object;
// 禁止new创建,统一走GuardObjectProxy类创建
private GuardedObject() {}
// 阻塞等待结果
// get可以传入超时参数给await使用,这里演示不添加了
public T get(Predicate<T> predicate) {
lock.lock();
try {
// MESA管程经典编程模式 while判断
while (!predicate.test(object)) {
done.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return object;
}
// 接收到数据时调用,某些框架里回调函数调用
public void onMessage(T object) {
lock.lock();
try {
this.object = object;
done.signalAll();
} finally {
lock.unlock();
}
}
// 管理GuardObject映射关系
public static class GuardObjectProxy<K, T> {
// 单例
private static volatile GuardObjectProxy<?, ?> object = null;
public Map<K, GuardedObject<T>> guardedObjectMap = new ConcurrentHashMap<>();
// 单例模式,使用一个guardedObjectMap
private GuardObjectProxy() {}
// 创建GuardObject
public GuardedObject<T> createGuardObject(K k) {
GuardedObject<T> guardedObject = new GuardedObject<>();
guardedObjectMap.put(k, guardedObject);
return guardedObject;
}
// 查找GuardObject
public GuardedObject<T> getGuardObject(K k) {
return guardedObjectMap.get(k);
}
// 获取GuardObjectProxy实例
public static GuardObjectProxy<?, ?> getProxy() {
if (Objects.isNull(object)) {
synchronized (GuardObjectProxy.class) {
if (Objects.isNull(object)) {
object = new GuardObjectProxy<>();
}
}
}
return object;
}
}
}
@Data
@AllArgsConstructor
class Message {
private Integer id;
private String content;
@Override
public String toString() {
return "messageId:" + id + " content:" + content;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 获取代理类单例
GuardedObject.GuardObjectProxy<Integer, String> guardedObjectProxy =
(GuardedObject.GuardObjectProxy<Integer, String>) GuardedObject.GuardObjectProxy.getProxy();
// 模拟线程发送线程池
ExecutorService sendExecutor = Executors.newFixedThreadPool(5);
// 模拟线程接收线程池
ExecutorService receiveExecutor = Executors.newFixedThreadPool(5);
// 模拟消息队列
BlockingQueue<Message> mq = new LinkedBlockingQueue<>(5);
// 异步发送4条消息并等待结果
for (int i = 1; i < 5; i++) {
Integer finalI = i;
sendExecutor.submit(() -> {
// 发送消息,message id为i
try {
Message message = new Message(finalI, "Message" + finalI);
GuardedObject<String> guardedObject = guardedObjectProxy.createGuardObject(finalI);
mq.put(message);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send] " + message + " success");
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send get wait] messageId:" + message.getId());
String receiveMessage = guardedObject.get(Objects::nonNull);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send get end] " + receiveMessage + "\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 接收消息并修改对应状态
for (int i = 0; i < 5; i++) {
receiveExecutor.submit(() -> {
while (true) {
try {
Message message = mq.take();
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [receive] " + message);
// 模拟消息处理时长,id为几就sleep几秒,观察接收阻塞情况
sleep(message.getId());
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [receive handle end] " + message);
guardedObjectProxy.getGuardObject(message.getId()).onMessage("handle **" + message.getContent() + "**");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
@SneakyThrows
public static void sleep(int time) {
TimeUnit.SECONDS.sleep(time);
}
}
打印如下,可以看到pool-1的发送线程池的线程,按预期等待对应时长并接收到结果。
20:32:00.087 pool-1-thread-2 [send] messageId:2 content:Message2 success
20:32:00.087 pool-2-thread-4 [receive] messageId:3 content:Message3
20:32:00.087 pool-1-thread-4 [send] messageId:4 content:Message4 success
20:32:00.087 pool-2-thread-3 [receive] messageId:1 content:Message1
20:32:00.087 pool-1-thread-4 [send get wait] messageId:4
20:32:00.087 pool-1-thread-3 [send] messageId:3 content:Message3 success
20:32:00.087 pool-2-thread-2 [receive] messageId:2 content:Message2
20:32:00.087 pool-1-thread-1 [send] messageId:1 content:Message1 success
20:32:00.087 pool-1-thread-3 [send get wait] messageId:3
20:32:00.087 pool-2-thread-1 [receive] messageId:4 content:Message4
20:32:00.087 pool-1-thread-2 [send get wait] messageId:2
20:32:00.087 pool-1-thread-1 [send get wait] messageId:1
20:32:01.093 pool-2-thread-3 [receive handle end] messageId:1 content:Message1
20:32:01.093 pool-1-thread-1 [send get end] handle **Message1**
20:32:02.093 pool-2-thread-2 [receive handle end] messageId:2 content:Message2
20:32:02.093 pool-1-thread-2 [send get end] handle **Message2**
20:32:03.093 pool-2-thread-4 [receive handle end] messageId:3 content:Message3
20:32:03.093 pool-1-thread-3 [send get end] handle **Message3**
20:32:04.093 pool-2-thread-1 [receive handle end] messageId:4 content:Message4
20:32:04.093 pool-1-thread-4 [send get end] handle **Message4**
4.3 总结
Guarded Suspension模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化。无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免 Bug。
但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,如上文所示。
5. 放弃模式(Balking)
5.1 特点
- 如果条件不满足就放弃执行,不像保护性暂挂模式一样一直等到超时。
- 仅执行一次初始化方法
- 尽量减少加锁范围,只针对状态读取的操作进行加锁处理
- 若是仅有主线程和一个子线程,分别修改不同状态,可以仅使用volatile修饰状态变量,而不加锁
5.2 例子
5.2.1 锁实现
@Slf4j
class Balking {
// 数据修改状态
private boolean state;
// Balking模式相关类初始化状态
private final AtomicBoolean initState = new AtomicBoolean(true);
// 举例用的定期执行的线程池
private ScheduledExecutorService executor;
public Balking() {
check();
}
// 随便写了个初始化方法,仅仅为了演示Balking模式只初始化一次
private void doInit() {
if (initState.getAndSet(false)) {
state = false;
executor = Executors.newScheduledThreadPool(2);
// 1秒后,每秒执行一次。这里使用两个线程进行竞争。
executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS);
executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS);
}
}
// 定期执行的方法
// 若是修改了数据就执行保存数据,否则直接放弃
private void check() {
doInit();
// 利用锁保证可见性/原子性/顺序性,最小粒度加锁,判断完状态就取消锁
synchronized (this) {
if (!state) {
log.info("无事发生 ");
return;
}
// 记得在临界区修改状态,不然存在竞态条件
state = false;
}
// 保存数据
saveData();
}
// 修改状态
public void changeState(Boolean state) {
// 利用锁保证可见性/原子性/顺序性
synchronized (this) {
this.state = state;
}
}
// 保存数据
private void saveData() {
log.info(" 保存数据");
}
// 修改数据
public void updateData() {
log.info(" 修改数据");
changeState(true);
}
}
public class Test {
public static void main(String[] args) {
Balking balking = new Balking();
// 等待3秒后修改数据
sleep(1);
balking.updateData();
// 再等待2秒后修改数据
sleep(2);
balking.updateData();
}
@SneakyThrows
public static void sleep(int time) {
TimeUnit.SECONDS.sleep(time);
}
}
打印如下。
21:48:40.603 [main] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
21:48:41.608 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
21:48:41.608 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
21:48:41.610 [main] INFO com.huaxiaogou.MDog.test.Balking - 修改数据
21:48:42.614 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 保存数据
21:48:42.614 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
21:48:43.615 [main] INFO com.huaxiaogou.MDog.test.Balking - 修改数据
21:48:43.617 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
21:48:43.617 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 保存数据
21:48:44.623 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
5.2.2 volatile实现
首先将之前代码加锁操作全部去掉。
其次没有了锁保证可见性,给state加上volatile。
// 数据修改状态
private volatile boolean state;
最后,将线程数调为1。由于本来就只有1个线程保存数据,也不会有什么问题。
executor = Executors.newSingleThreadScheduledExecutor();
// 1秒后,每秒执行一次
executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS);
6. Thread-Per-Message模式
每个请求开一个线程单独处理,但是由于Java中的线程都是重量级线程,这个模式并不适用Java代码。
一般都是利用线程池重复使用线程。
JDK21支持虚拟线程了,其实就是协程。这个模式后续可能会有所发展。
7. Worker Thread模式
结构
对应Java中的应用其实就是常用的线程池,更多的线程池信息可以参考线程池详解。
7.2. 两阶段终止模式
-
设置一个标志位
终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。Thread 类提供的 interrupt() 方法可以实现。 -
响应终止指令
线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。
更多的线程中断信息可以参考线程池子线程的终止Shutdown()、ShutdownNow()
8. 生产者-消费者模式
特点:
- 支持批量执行以提升性能
- 支持分阶段提交以提升性能
优点:
- 功能解耦
- 支持异步,能够平衡生产者和消费者的速度差异
8.1 支持批量执行以提升性能
比如有个服务,每来一个数据都需要insert一次,若是数据量较多频繁的insert会影响数据库性能。可以考虑丢入队列,消费者取出批量数据并通过批量插入接口一次调用插入。(仅举例,这样会有数据丢失风险)
8.2 支持分阶段提交以提升性能
将数据写入阻塞队列,子线程根据不同规则读取后进行不同处理。