Java并发编程实战③-并发设计模式

[TOC]

Java并发编程实战-并发设计模式

Java并发编程实战是学习《并发编程实战》和极客时间《Java并发编程》的记录。

并发设计模式-总览

  1. lmmutability模式:如何利用不变性解决并发问题?
  2. Copy- on-Write模式:不是延时策略的COW
  3. 线程本地存储模式:没有共享,就没有伤害
  4. Guarded Suspension模式:等待唤醒机制的规范实现
  5. Balking模式:再谈线程安全的单例模式
  6. Thread- -Per- -Message模式:最简单实用的分工方法
  7. Worker Thread模式:如何避免重复创建线程?
  8. 两阶段终止模式:如何优雅地终止线程?
  9. 生产者-消费者模式:用流水线思想提高效率

lmmutability模式:如何利用不变性解决并发问题?

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

解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。

快速实现具备不可变性的类

  • 将一个类所有的属性都设置成final的,并且只允许存在只读方 法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是final的(不能被继承) 。
  • Java SDK里很多类都具备不可变性 (因此线程安全):String、Integer、Double等基础类型的包装类都具备不可变行。
    • 类和属性都是final的,所有方法均是只读的。
    • 如果需要修改对象,创建一个新的不可变对象 。
    • 如何解决对象太多,浪费内存?利用享元模式避免创建重复对象

利用享元模式避免创建重复对象

享元模式(Flyweight Pattern ):利用享元模式可 以减少创建对象的数量,从而减少内存占用 。

  • 享元模式本质上其实就是一个对象池。
  • Long内部维护了一个静态的对象池,仅缓存了[-128,127]之间的数字 。

使用Immutability模式的注意事项

  1. 对象的所有属性都是final的,并不能保证不可变性;
  2. 不可变对象也需要正确发布。
  3. 在使用Immutability模式的时候一定要确认保持不变性的边界在哪里,是 否要求属性对象也具备不可变性。
  4. 配合原子类的cas机制➕不可变来实现线程安全。

总结

建议当你试图解决一个并发问题时, 可以首先尝试一下Immutability模式,看是否能够快速解决。

其实还有一种更简单的 不变性对象,那就是无状态

Copy- on-Write模式:不是延时策略的COW

Copy-on-Write,经常被称为COW,也就是写时复制。在不可变性模式下就是使用这种写时复制实现的。写时复制在很多领域都有使用。

使用Copy-on-Write更多地体现的是一种延时策 略,只有在真正需要复制的时候才复制,而不是提前复制好 。

Copy-on-Write最大的应用领域还是在函数式编程领域。

COW的真实案例

RPC框架路由功能:一个服务由多实例分布式部署。

客服端在请求这个服务的时候要做负载均衡。

路由表 :对读的性能要求很高,读多写少,弱一致性 。

小科普:5秒钟,对于以纳秒作为时钟 周期的CPU来说,那何止是一万年,所以路由表对一致性的要求并不高 。

//todo:路由表的代码

线程本地存储模式:没有共享,就没有伤害

  • 多个线程同时读写同一共享变量存在并发问题。 不可变性模式和cow模式都是围绕写问题出发的。其实还可以突破共享变量,没有共享变量也不会有并发问题,正所 谓是没有共享,就没有伤害。
  • 线程封闭 ,局部变量可以做到避免共享 ,Java语言提供的 线程本地存储(ThreadLocal)就能够做到。

ThreadLocal的使用方法

  • ThreadLocal使用正确姿势
/**
* 静态内部类
*/
static class ThreadId {
//原子类和threadLocal配合使用
static final AtomicInteger nextId = new AtomicInteger(0);
static final ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> nextId.getAndIncrement());

static Integer get() {
return tl.get();
}
}

/**
* 静态线程安全:java.text.SimpleDateFormat
*/
static class SafeDateFormat {
static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

static DateFormat get() {
return tl.get();
}

}

ThreadLocal的工作原理

  • 在Java的实现方案里面,ThreadLocal仅仅是一个代理工具类,内部并不持有任何与线 程相关的数据,所有和线程相关的数据都存储在Thread里面,这样的设计容易理解。而从数据的亲缘性上 来讲,ThreadLocalMap属于Thread也更加合理。
  • 不容易产生内存泄露 。而且ThreadLocalMap里对ThreadLocal的引用还是弱引用 (WeakReference) 。所以只要Thread对象可以被回收,那么ThreadLocalMap就能被回收。Java的这种实 现方案虽然看上去复杂一些,但是更加安全。

image-20191024091910152

ThreadLocal与内存泄露

  • 在线程池中使用ThreadLocal可能导致内存泄露?因为线程池中的线程生命周期比较长。导致Thread持有的ThreadLocalMap一直无法被回收。
  • 解决方案:try{}finally{}方案了,这个简直 就是手动释放资源的利器。

InheritableThreadLocal与继承性

  • 通过ThreadLocal创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过ThreadLocal创建了线 程变量V,而后该线程创建了子线程,你在子线程中是无法通过ThreadLocal来访问父线程的线程变量V的。
  • InheritableThreadLocal 能够解决上面问题。但是非常不建议在线程池中使用该类。

总结

在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。避免共享有两种方案,一种方案 是将这个工具类作为局部变量使用,另外一种方案就是线程本地存储模式。这两种方案,局部变量方案的缺 点是在高并发场景下会频繁创建对象,而线程本地存储方案,每个线程只需要创建一个工具类的实例,所以 不存在频繁创建对象的问题。

在线程池中使用ThreadLocal仍可能导致内存泄漏 。解决方案:try{}finally{}方案了

Guarded Suspension模式:等待唤醒机制的规范实现

消息队列(MQ):主要用作流量削峰和系统解耦 。学会根据现实世界模型来建模

Guarded Suspension模式

  • Guarded Suspension模式:保护性地暂停。

image-20191024094651006

  • GuardedObject的内部实现非常简单,是管程的一个经典用法,你可以参考下面的示例代码,核心是:get() 方法通过条件变量的await()方法实现等待,onChanged()方法通过条件变量的signalAll()方法实现唤醒功能。

总结

Guarded Suspension模式本质上是一种等待唤醒机制的实现,只不过Guarded Suspension模式将其规范化 了。

但Guarded Suspension模式在解决实际问题的时候,往往还是需要扩展的 。

Guarded Suspension模式的别名:多线程版本的if。

Balking模式:再谈线程安全的单例模式