并发设计模式

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)),值为本地存储的泛型值。

image

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)方法。

image

经典实现如下。

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是异步的那么调用也是异步的,等待是实现了异步转同步)

image

首先由于是不同线程,为了能匹配上,需要利用一个唯一的标识。这里可以利用消息的唯一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模式

结构

image

对应Java中的应用其实就是常用的线程池,更多的线程池信息可以参考线程池详解

7.2. 两阶段终止模式

  • 设置一个标志位
    终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。Thread 类提供的 interrupt() 方法可以实现。

  • 响应终止指令
    线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。

更多的线程中断信息可以参考线程池子线程的终止Shutdown()、ShutdownNow()

8. 生产者-消费者模式

特点:

  • 支持批量执行以提升性能
  • 支持分阶段提交以提升性能

优点:

  • 功能解耦
  • 支持异步,能够平衡生产者和消费者的速度差异

8.1 支持批量执行以提升性能

比如有个服务,每来一个数据都需要insert一次,若是数据量较多频繁的insert会影响数据库性能。可以考虑丢入队列,消费者取出批量数据并通过批量插入接口一次调用插入。(仅举例,这样会有数据丢失风险)

8.2 支持分阶段提交以提升性能

将数据写入阻塞队列,子线程根据不同规则读取后进行不同处理。

posted @ 2023-12-04 20:01  kiper  阅读(60)  评论(0编辑  收藏  举报