读书笔记 - Java 多线程编程实战指南
第三张 Immutable Object(不可变对象)模式
模式简介
- 多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。这些同步访问控制,如显式锁(Explicit Lock)和 CAS(Compare and Swap)操作,会带来额外的开销和问题,如上下文切换、等待时间和 ABA 问题等。
- Immutable Object 模式使得我们可以在不使用锁的情况下,即保证共享变量访问的线程安全,又能避免引入锁可能带来的问题和开销。
- Immutable Object 模式的意图是通过使用对外可见的状态不可变的对象,使得被共享的对象“天生”具有线程安全性,而无须额外地同步访问控制。
- 所谓状态不可变的对象,即对象一经创建,其对外可见的状态就保持不变。
-
模式架构
一个严格意义上不可变对象要满足一下所有条件:
- 类本身使用 final 修饰:防止其子类改变其定义的行为
- 所有字段都是用 final 修饰的:使用 final 修饰不仅仅是从语义上说明被修饰字段的引用不可改变。更重要的是这个语义在多线程环境下由 JMM(Java Memory Model)保证了被修饰字段所引用对象初始化安全,即 final 修饰的字段在其他线程可见时,它必定时初始化完成的。相反,非 final 修饰的字段由于缺少这种保证,可能导致一个线程“看到”一个字段的时候,它还未被初始化完成,从而可能导致一些不可预料的结果。
- 在对象的创建过程中,this 关键字没有泄漏给其他类:防止其他类(如该类的内部匿名类)在对象创建过程中修改其状态。
- 任何字段,若其引用了其他状态可变的对象(如集合、数组等),则这些字段必须是 private 修饰的,而且这些字段值不能对外暴露。若有相关方法要返回这些字段值,应该进行防御性复制(Defensive Copy)。
-
模式案例
某彩信网管系统在处理由增值业务提供商(VASP, Value-Added Service Provider)下发给手机终端用户的彩信消息时,需要根据彩信接收方号码的前缀(如 1381234)选择对应的彩信中心(MMSC,Multimedia Messaging Service Center),然后转发消息给选中的彩信中心,由其负责对接电信网络将彩信消息下发给手机终端用户。彩信中心相对于彩信网关系统而言,它是一个独立的部件,二者通过网络进行交互。这个选择彩信中心的过程,我们称之为路由(Routing)。而手机号前缀和彩信中心的这种对应关系,被称为路由表。路由表在软件运维过程中可能发生变化。例如,业务扩容带来的新增彩信中心、为某个号码前缀执行新的彩信中心等。虽然路由表在该系统中是由多线程共享的数据,但是这些数据的变化频率并不高。因此,即使是为了保证线程安全,我们也不希望对这些数据的访问进行加锁等并发访问控制,以免产生不必要的开销和问题。这是,Immutable Object 模式就派上用场了。
本案例Demo详见 multithreading 工程的 ImmutableObject 包。
-
Immutable Object 模式的评价与实现考量
不可变对象具有天生的线程安全性,多个线程共享一个不可变对象的时候无须使用额外的并发访问控制,这使得我们可以避免显式锁等并发访问控制的开销和问题,简化了多线程编程。
Immutable Object 模式特别适用于以下场景:
- 被建模对象的状态变化不频繁
- 同时对一组相关的数据进行写操作,因此需要保证原子性
- 使用某个对象作为安全的HashMap的Key
Immutable Object 模式实现时需要注意以下几个问题:
- 被建模对象的状态变更比较频繁: 此时也不见得不能使用 Immutable Object 模式。只是这意味着频繁创建新的不可变对象,因此会增加 JVM 垃圾回收(Garbage Collection)的负担和 CPU 消耗,我们需要综合考虑:被建模对象的规模、代码目标运行环境的 JVM 内存分配情况、系统对吞吐率和响应性的要求。若这几个方面因素综合考虑都能满足要求,那么使用不可变对象建模也未尝不可。
- 使用等效或者近似的不可变对象: 有时创建严格意义上的不可变对象比较难,但是尽量向严格意义上的不可变对象靠拢也有利于发挥不可变对象的好处。
- 防御性复制: 如果不可变对象本身包含一些状态需要对外暴露,而对应的字段本身有是可变的(如 HashMap),那么返回这些字段的方法还是需要做防御性复制,以避免外部代码修改了其内部状态。
-
Java 标准库实例
- JDK 1.5 中引入的类 java.util.concurrent.CopyOnWriteArrayList 应用了 Immutable Object 模式,使得对 CopyOnWriteArrayList 实例进行遍历时不用加锁也能保证线程安全。
- 当然,CopyOnWriteArrayList 也不是“万能”的,它是专门针对遍历操作的频率比添加和删除操作更加频繁的场景设计的。