对象的组合
一、设计线程安全的类
1.1 设计过程
1.1.1 收集对象状态
- 收集类对象的所有状态
- 如果对象中引用了其他对象,那么该对象的状态也包括引用对象的状态
- 如果发布了某个可变对象的引用,那么就不应该算作对象的状态
1.1.2 收集同步需求
- 识别对象状态中哪些需要同步处理,即可能被多线程访问的状态
- 找出对象状态的先验条件、不变性条件、后验条件
- 如果不了解对象的不变性条件和后验条件,那么就不能确保线程安全性
1)先验条件:验证是否可以进行状态迁移
状态迁移前,先验证某些条件是否满足,满足则继续:
1 2 3 4 5 6 7 8 9 10
| class Counter { private boolean isReady = false; private int count; public synchronized void increment() { if (!isReady) { return; } count++; } }
|
先验条件,主要是验证状态迁移前是否满足条件,比如 if (!isReady)
。
2)不变性条件:状态值是否是有效的
针对状态变量本身进行验证,验证值是否有效:
1 2 3 4 5 6 7 8 9
| class Counter { private int count; public synchronized void setCount(int count) { if (count < 0) { throw new IllegalArgumentException("count can not be negative"); } this.count = count; } }
|
count >= 0
属于有效状态;count < 0
属于无效状态,所以 count 状态的不变性条件是:count >= 0
。
不变性条件,着重验证状态值是否有效,不能超出它设定的值域范围,比如这里的 count >= 0
。
3)后验条件:状态迁移后的值验证
状态迁移后,验证某些条件是否还满足,满足则完成状态迁移:
1 2 3 4 5 6 7 8 9 10
| class NumberRange { private int min = 0; private int max = 0; public synchronized void setMin(int min) { if (min > this.max) { return; } this.min = min; } }
|
这里,min 传入的值是有效的,即满足不变性条件。
但是它还有额外的约束条件,那就是 min <= max
,这就属于后验条件,用于验证状态迁移的有效性。
1.1.3 选择状态的并发访问策略
- 选择状态的并发访问策略,以便确保状态的正确性
- 比如使用原子类、加锁等
1.2 线程安全实现-实例封闭
- 将状态都封装在对象内部
- 限制只能通过对象的方法访问状态
1.2.1 共享监视器模式
将当前对象作为锁对象,外部也可以使用:
1 2 3 4 5 6
| class ShareSync { private int count = 0; public synchronized void increment() { count++; } }
|
使用状态对象作为锁,那么在类外面也可以使用这个锁:
1 2 3 4 5 6 7
| class Client { public void doSomething(ShareSync sync) { synchronized (sync) { } } }
|
这种方式会将锁暴露给外面,是一种共享的监视器模式。
不过,外部代码如果错误地获取锁对象,有可能会产生活跃性问题。
1.2.2 私有监视器模式
对象内部使用私有的监视器,外部不能访问:
1 2 3 4 5 6 7 8 9 10
| class PrivateSync { private final Object lock = new Object();
private int count = 0; public void increment() { synchronized (lock) { count++; } } }
|
由于加锁对象是私有的,在类外面也不能使用这个锁,是一种私有的监视器模式。
1.2.3 重入锁模式
除了使用监视器,还可以使用重入锁:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class ReentrantSync { private final ReentrantLock lock = new ReentrantLock();
private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } }
|
重入锁模式,除了写法有些不同,功能上和监视器模式差不多。
和监视器模式一样,重入锁模式也可以分为共享模式和私有模式。
二、线程安全类的组合
当从头构建一个类,或者将多个非线程安全的类组合在一起时,同步策略是相当有用的。
但是如果类中的各个状态都是线程安全的,那这个时候是否还需要同步策略?
2.1 单状态组合
- 单状态是普通类型,需要同步策略,由组合提供同步
- 单状态是线程安全类,可以由自身保证线程安全性,无需额外同步策略
普通状态:
1 2 3 4 5 6
| class SingleState { private int count = 0; public synchronized void increment() { count++; } }
|
线程安全状态:
1 2 3 4 5 6
| class SingleSafeState { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.getAndIncrement(); } }
|
2.2 多独立状态组合
- 相互独立的多个普通状态,需要同步策略,由组合提供同步
- 相互独立的多个线程安全状态,无需额外的同步策略,由状态自身保证线程安全性
普通独立状态:
1 2 3 4 5 6 7 8 9 10
| class MultiState { private int count = 0; private boolean ready = false; public synchronized void increment() { count++; } public synchronized void setReady() { ready = true; } }
|
线程安全独立状态:
1 2 3 4 5 6 7 8 9 10
| class MultiSafeState { private AtomicInteger count = new AtomicInteger(0); private AtomicBoolean ready = new AtomicBoolean(false); public void increment() { count.getAndIncrement(); } public void setReady() { ready.getAndSet(true); } }
|
2.3 多关联状态组合
- 有关联/依赖关系的多个状态,必须要同步策略,由组合提供同步
- 关联关系:先验条件、不定性条件、后验条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class MultiRelationState { private int min = 0; private int max = 0; public synchronized void setMin(int min) { if (min > this.max) { return; } this.min = min; } public synchronized void setMax(int max) { if (max < this.min) { return; } this.max = max; } }
|
2.4 公开状态组合
- 组合在一起的状态,以公开的方式发布给外部,实现状态共享
- 组合不需要再提供同步策略,因为已经不算在自己的管辖范围内了
- 由外部代码自行对发布的共享状态添加同步策略
对外发布共享状态:
1 2 3 4
| class MultiPublicState { public volatile int count = 0; public AtomicBoolean ready = new AtomicBoolean(false); }
|
外部自己控制同步策略:
1 2 3 4 5 6 7 8 9 10 11
| class Client { private MultiPublicState state; public void increment() { synchronized (state) { state.count++; } } public void setReady() { state.ready.getAndSet(true); } }
|
三、线程安全类的扩展
- 如何给线程安全类添加新的方法?
- 保证原子性、保证线程安全性?
3.1 客户端加锁机制
客户端代码自己控制同步策略,实现所需功能:
1 2 3 4 5 6 7 8 9 10
| class Client { private Vector<Integer> list; public void putIfAbstent(int value) { synchronized (list) { if (!list.contains(value)) { list.add(value); } } } }
|
优点:不需要修改原始类的代码。
缺点:所有用到这个方法的地方,都要重复写一份同样的代码,容易出错。
3.2 修改原始类源码
在有源码修改权的情况下,直接在原始类上添加线程安全方法:
1 2 3 4 5 6 7
| class Vector<E> { public synchronized void putIfAbstent(E value) { if (!contains(value)) { add(value); } } }
|
优点:实现简单,封装性好,是最安全的。
缺点:第三方的代码基本上没办法修改。
3.3 扩展原始类
在原始类支持继承扩展的情况下,直接继承添加新方法:
1 2 3 4 5 6 7
| class ChildVector<E> extends Vector<E> { public synchronized void putIfAbstent(E value) { if (!contains(value)) { add(value); } } }
|
优点:实现简单,可维护性好。
缺点:继承依赖于原始类对象,一旦原始类发生变更,继承类也需要同时更新;不是所有类都支持继承;继承还会增加类结构的复杂度。
3.4 代理原始类对象
使用组合代理原始对象,包装一层:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class VectorProxy<E> { private Vector<E> vector; public VectorProxy(Vector<E> vector) { this.vector = vector; } public void putIfAbstent(E value) { synchronized (vector) { if (!vector.contains(value)) { vector.add(value); } } } }
|
优点:实现简单,封装性好,可维护性好。
缺点:对象类型发生变化,所有用到原始类的地方都需要修改。多加了一层,可能有轻微的性能损失。
总结
设计线程安全的类:
- 收集对象状态
- 收集同步需求
- 选择状态的并发访问策略
实例封闭:
线程安全类的组合:
- 单状态组合
- 多独立状态组合
- 多关联状态组合
- 公开状态组合
线程安全类的扩展:
- 客户端加锁机制
- 修改原始类源码
- 扩展原始类
- 代理原始类对象