赞
踩
随便扯扯,刚开始我学到这里的时候,我有点茅舍顿开的感觉。怎么说呢,比如我现在呆的公司,有一个批量导出订单excel的功能。这个功能前员工已经实现了,当时我在阅读这部分代码的时候就觉得写这个代码的人很吊。他在代码中,利用多线程并发的批量请求数据,多线程封装符合导出格式的数据,而此时,他不是直接就写到excel中,而是先到一个阻塞队列中,然后另外开一个线程从队列中读数据写入到excel中(还有很多细节)。当时我就在想,他是怎么想到这种处理方式。原来这就是一种并发设计模式,就是下面会讲到的生产者-消费者模式。
23种设计模式了解过吧,它和一般的23种设计模式一样,并发设计模式是前人解决并发问题的经验总结。
比较常用的有9中,我把他们归为三类。
避免共享的设计模式:Immutability 模式、Copy-on-Write 模式和线程本地存储模式本
多线程版本 IF 的设计模式:Guarded Suspension 模式和 Balking 模式
三种最简单的分工模式:Thread-Per-Message 模式、Worker Thread 模式和生产者 - 消费者模式
不变性(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的不变类。
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();
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。