- 避免共享:Immutability模式、Copy-on-Write模式、Thread-Local-Storage模式
- 高效协作:Guarded Suspension模式、Balking模式
- 合理分工:Thread-Per-Message模式、Worker Thread模式、生产者-消费者模式
- 优雅关闭:两阶段终止模式
1、Immutability模式
不变性,即对象一旦被创建,状态就不再发生变化。
具备不可变性的类
类和属性都是final的,所有方法均是只读的。
Java中常用的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性。String类中修改字符串的方法replace的原理是,生成一个新的不可变对象,修改之后作为方法的返回值。而可变对象往往是修改自己的属性值。所有的修改操作都需要创建一个新的不可变对象,创建大量的对象会浪费内存。利用享元模式(Flyweight Pattern)可以减少创建对象的数量,从而减少内存占用。Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。
享元模式
享元模式中有一个对象池,在创建对象之前,先判断对象池中有没有需要的对象实例,如有则直接从对象池中获取,如没有则新建一个对象并将其放入对象池中。Long内部维护了一个静态的对象池(LongCache),仅缓存了[-128,127]之间的数字,这个对象池在 JVM 启动的时候就创建好了,而且这个对象池一直都不会变化,也就是说它是静态的。之所以采用这样的设计,是因为 Long 这个对象的状态共有 2的64次方种,实在太多,不宜全部缓存,而[-128,127]之间的数字利用率最高。所有的基础类型的包装类都不适合做锁,就是因为它们内部用到了享元模式,这会导致看上去私有的锁,其实是共有的。
使用享元模式时需要注意的点:
即使一个类的所有属性都是final的,也有可能是可变的。如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。在使用 Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性。不可变对象虽然是线程安全的,但是并不意味着引用这些不可变对象的对象就是线程安全的。
具备不变性的对象,只有一种状态,这个状态由对象内部所有的不变属性共同决定。其实还有一种更简单的不变性对象,那就是无状态。无状态对象内部没有属性,只有方法。除了无状态的对象,你可能还听说过无状态的服务、无状态的协议等等。无状态有很多好处,最核心的一点就是性能。在多线程领域,无状态对象没有线程安全问题,无需同步处理,自然性能很好;在分布式领域,无状态意味着可以无限地水平扩展,所以分布式领域里面性能的瓶颈一定不是出在无状态的服务节点上。
2、Copy-on-Write 模式
写时复制(Copy-on-Write, COW或者CoW)是一种延时策略,只要在真正需要复制的时候才复制,而不是提前复制好。同时还支持按需复制,COW通常用于操作系统领域提升性能。在操作系统领域,除了创建进程用到了 Copy-on-Write,很多文件系统也同样用到了,例如 Btrfs (B-Tree File System)、aufs(advanced multi-layered unification filesystem)等。相比较而言,Java 提供的 Copy-on-Write 容器,由于在修改的同时会复制整个容器,所以在提升读操作性能的同时,是以内存复制为代价的。除了上面我们说的 Java 领域、操作系统领域,很多其他领域也都能看到 Copy-on-Write 的身影:Docker 容器镜像的设计是 Copy-on-Write,甚至分布式源码管理系统 Git 背后的设计思想都有 Copy-on-Write
不过,Copy-on-Write 最大的应用领域还是在函数式编程领域。函数式编程的基础是不可变性(Immutability),所以函数式编程里面所有的修改操作都需要 Copy-on-Write 来解决。你或许会有疑问,“所有数据的修改都需要复制一份,性能是不是会成为瓶颈呢?”你的担忧是有道理的,之所以函数式编程早年间没有兴起,性能绝对拖了后腿。但是随着硬件性能的提升,性能问题已经慢慢变得可以接受了。而且,Copy-on-Write 也远不像 Java 里的 CopyOnWriteArrayList 那样笨:整个数组都复制一遍。Copy-on-Write 也是可以按需复制的,如果你感兴趣可以参考Purely Functional Data Structures这本书,里面描述了各种具备不变性的数据结构的实现。 纯函数式数据结构 (Purely Functional Data Structures) 指的是那些具有不变性的高效数据结构。
CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。总而言之,Copy-on-Write适合对读的性能要求很高,读多写少,弱一致性场景。如Dubbo中的路由表。
3、线程本地存储模式(ThreadLocal)
线程封闭的本质就是避免共享,除了局部变量,还有Java提供的线程本地存储(ThreadLocal)也可以实现。Java中的设计方案:类Thread拥有threadLocals属性,threadLocals属性的类型是ThreadLocalMap容器,ThreadLocalMap容器中以ThreadLocal为key,维护不同类型的value。ThreadLocalMap 里对 ThreadLocal 的引用是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。这样不容易产生内存泄露。
在线程池中使用ThreadLocal很容易产生内存泄漏,原因在于线程池中线程的存活时间过长,往往都是和程序同生共死的。这意味着Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用(WeakReference),所以只要ThreadLocal结束了自己的生命周期就可以被回收掉。但Entry中的Value是被Entry强引用的,所以即便Value的生命周期结束了,Value也是无法被回收的,从而导致内存泄漏。
那么在线程池中,我们需要自己手动释放对Value的强引用,可以使用try{}finally{}方案。
通过ThreadLocal创建的线程变量,其子线程是无法继承的,也就是通过ThreadLocal创建了线程变量V,后续该线程创建了子线程,而这个子线程中无法通过访问ThreadLocal来访问父线程的线程变量V。Java提供了InheritableThreadLocal来支持这种特性,InheritableThreadLocal是ThreadLocal的子类,所以用法和ThreadLocal相同。我完全不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。
4、Guarded Suspension模式:等待唤醒机制的规范实现
Guarded Suspension可译为保护暂停,当服务进程准备好时,才提供服务。服务器可能在短时间内承受大量的客户端请求,但是每个请求都很重要不能丢弃,客户端只能排队等待服务端处理。
Guarded Suspension 模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化了。规范化的好处是你无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免一不小心写出个 Bug 来。但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,本篇文章就直接对 GuardedObject 的功能进行了增强,Dubbo 中 DefaultFuture 这个类也是采用的这种方式,你可以对比着来看,相信对 DefaultFuture 的实现原理会理解得更透彻。当然,你也可以创建新的类来实现对 Guarded Suspension 模式的扩展。Guarded Suspension 模式也常被称作 Guarded Wait 模式、Spin Lock 模式(因为使用了 while 循环去等待)。
5、生产者-消费者模式
生产者-消费者模式两大优点:解耦、支持异步。
- 生产者和消费者只通过任务队列通信,没有任何依赖关系。
- 队列中任务的生产和消费是异步的,即生产线程只需要将任务添加到任务队列而无需等待任务被消费者线程消费完。
Java中提供的线程池本身就是就是一种生产者-消费者模式,可以满足大部分并发场景需求。但是如数据库批量提交、分段提交的场景,需要自定义实现。
网友评论