4.1 设计线程安全的类
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
如果对象中的所有的域都是基本类型的变量,那么这些域将构成对象的全部状态
- 对于含有n个基本类型的对象,其状态就是这些域构成的n元组。
如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。
4.1.1 收集同步需求
对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final类型的域使用得越多,就越能简化对象可能状态的分析过程。
不变性条件:用于判断状态是否有效
后验条件:用于判断状态迁移是否有效
如果某些状态是无效的,那么必须对底层的变量进行封装,否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。**
4.1.2 依赖状态的操作
先验条件:在调用方法之前必须为真的条件
后验条件:方法顺利执行完毕之后必须为真的条件
依赖状态的操作:操作中包含有基于状态的先验条件
- eg. 不能从空队列中移除一个元素
在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但在并发程序中,先验条件可能会由于其他线程执行的操作而变成真。在并发程序中要一直等到先验条件为真,然后再执行该操作。**
实现:
- 阻塞队列(BlockingQueue…)
- 信号量(Semaphore)
- …
4.1.3 状态的所有权
4.2 实例封闭
当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。**
实例封闭是构建线程安全类的一个最简单方式。
实例封闭还使得不同的状态变量可以由不同的锁来保护。
Java的包装器工厂(eg. Collections.synchronizedList),只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。对底层容器对象的所有访问必须通过包装器来进行。
当发布其他对象时,例如迭代器或内部的类实例,可能会间接地发布被封闭对象,同样会使被封闭对象逸出。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。
4.2.1 Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
Java监视器模式的主要优势在于它的简单性。细粒度的加锁策略则可以提高伸缩性。
私有锁
4.2.2 示例:车辆追踪
4.3 线程安全性的委托
4.3.1 示例:基于委托的车辆追踪器
4.3.2 独立的状态变量
多个变量之间是彼此独立的,则可以将线程安全性委托给多个状态变量。
组合而成的类不会在其包含的多个状态变量上增加任何不变性条件。
4.3.3 当委托失效时
如果某个类含有复合操作,那么仅靠委托不足以实现线程安全性。在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
4.3.4 发布底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
4.3.5 示例:发布状态的车辆追踪器
4.4 在现有的线程安全类中添加功能
直接修改原始的类;
扩展原始类
- 扩展方法更加脆弱。现在同步策略被分布到多个单独维护的源代码文件中,如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。
4.4.1 客户端加锁机制
第三种策略:扩展类的功能
- 将扩展代码放入一个“辅助类”中
相比扩展,客户端加锁更加脆弱。它将类C的加锁代码放到与C完全无关的其他类中。
4.4.2 组合
通过类似委托的方式来实现
额外的同步层可能导致轻微的性能损失,但更为健壮。
4.5 将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。