并发设计模式(一)

避免共享的设计模式

随便扯扯,刚开始我学到这里的时候,我有点茅舍顿开的感觉。怎么说呢,比如我现在呆的公司,有一个批量导出订单excel的功能。这个功能前员工已经实现了,当时我在阅读这部分代码的时候就觉得写这个代码的人很吊。他在代码中,利用多线程并发的批量请求数据,多线程封装符合导出格式的数据,而此时,他不是直接就写到excel中,而是先到一个阻塞队列中,然后另外开一个线程从队列中读数据写入到excel中(还有很多细节)。当时我就在想,他是怎么想到这种处理方式。原来这就是一种并发设计模式,就是下面会讲到的生产者-消费者模式。

23种设计模式了解过吧,它和一般的23种设计模式一样,并发设计模式是前人解决并发问题的经验总结。

比较常用的有9中,我把他们归为三类。
避免共享的设计模式:Immutability 模式、Copy-on-Write 模式和线程本地存储模式本
多线程版本 IF 的设计模式:Guarded Suspension 模式和 Balking 模式
三种最简单的分工模式:Thread-Per-Message 模式、Worker Thread 模式和生产者 - 消费者模式

避免共享的设计模式

Immutability 模式(不变性模式)

不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化

他是怎么解决并发问题的?先来看个例子:

public class Customer {
  public String name;
  public String email;
 
  public Customer(String name, String email) {
    this.name = name;
    this.email = email;
  }
 
  public void updateNameAndEmail(String newName, String newEmail) {
    this.name = newName;
    this.email = newEmail;
  }
 
  public void sendEmail() {
    System.out.println(String.format("Send email to %s with email address %s", this.name, this.email);
  }
}

当我们有两个线程几乎同时执行上面的 updateNameAndEmail() 和 sendEmail() 方法。就会存在并发问题,Customer 的可变性造成了不一致性。

那怎么处理呢?

将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。

类和属性都是 final 的,所有方法均是只读的。

那如果真的需要提供类似修改的功能呢。可以参看String,Integer的实现,创建一个新的不可变对象。

所有的修改操作都创建一个新的不可变对象,你可能会有这种担心:是不是创建的对象太多了,有点太浪费内存呢?是的,这样做的确有些浪费,那如何解决呢?

利用享元模式避免创建重复对象。Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。

注意事项

对象的所有属性都是 final 的,并不能保证不可变性。因为有些属性是对象类型,而我们可以修改对象类型里的属性。

所以在编写不可变类时需要注意对象属性的不可变性。

应用场景

不变模式的应用非常常见,Java语言中的String类和Integer等等都是不变模式的应用。String类所持有的字符串必须在构造String对象的时候指定,一旦String对象生成了,字符串内容就不能再改变。
尽管改变,是返回一个新的对象。在一定程度上,降低了对该对象进行并发访问时的同步化开销。另外。我觉得,在我实际开发中,创建一个不变类的场景应该很少,至少以我现在的工作阅历,我还没有遇到过这些场景,一般再适合的场景适用JDk的不变类。

Copy-on-Write模式

String 的 replace() 方法,并没有更改原字符串里面 value[]数组的内容,而是创建了一个新对象字符串,这种方法在解决不可变对象的修改问题时经常用到。实际上它本质上是一种 Copy-on-Write 方法。初次之外,并发集合类中,CopyOnWriteArrayList,CopyOnWriteArraySet等也是Copy-on-Write模式的一种应用

思想

修改时复制新对象,copy新对象后,原对象变量引用指向新的对象。在copy过程中,并发访问还是原对象中读取。所以会存在暂时的数据不一致。另外还有点消耗内存,每次修改都需要复制一个新的对象出来,好在随着自动垃圾回收(GC)算法的成熟以及硬件的发展,这种内存消耗已经渐渐可以接受了。

线程本地存储模式

并发中避免共享,还有一种比较靠谱的方案,线程本地存储模式,毕竟没有共享,就没有伤害。
ThreadLocal 就是线程本地存储模式的一中实现。可以理解 ThreadLocalMap 就是 Thread 的一个属性。

class Thread {
  //内部持有ThreadLocalMap
  ThreadLocal.ThreadLocalMap 
    threadLocals;
}
内存泄漏问题

然而线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收。

再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了


ExecutorService es;
ThreadLocal tl;
es.execute(()->{
  //ThreadLocal增加变量
  tl.set(obj);
  try {
    // 省略业务逻辑代码
  }finally {
    //手动清理ThreadLocal 
    tl.remove();
  }
});
应用场景

很多时候,我们需要在并发场景中使用一个线程不安全的工具类,比如 SimpleDateFormat。我们一般在使用SimpleDateFormat时为了避免共享,一般都把SimpleDateFormat对象定义在方法内部,作为局部变量。然而,这中方式在高并发场景下会频繁创建对象。所以,针对这中场景还有另外一种方案就是线程本地存储模式。每个线程只需要创建一个工具类的实例,所以不存在频繁创建对象的问题。

static class SafeDateFormat {
  //定义ThreadLocal变量
  static final ThreadLocal<DateFormat>
  tl=ThreadLocal.withInitial(
    ()-> new SimpleDateFormat(
      "yyyy-MM-dd HH:mm:ss"));
      
  static DateFormat get(){
    return tl.get();
  }
}
//不同线程执行下面代码
//返回的df是不同的
DateFormat df =
  SafeDateFormat.get();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值