04_对象的组合

对象的组合

一、设计线程安全的类

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) {
// do something
}
}
}

这种方式会将锁暴露给外面,是一种共享的监视器模式。

不过,外部代码如果错误地获取锁对象,有可能会产生活跃性问题。

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);
}
}
}
}

优点:实现简单,封装性好,可维护性好。

缺点:对象类型发生变化,所有用到原始类的地方都需要修改。多加了一层,可能有轻微的性能损失。

总结

设计线程安全的类:

  • 收集对象状态
  • 收集同步需求
  • 选择状态的并发访问策略

实例封闭:

  • 共享监视器模式
  • 私有监视器模式
  • 重入锁模式

线程安全类的组合:

  • 单状态组合
  • 多独立状态组合
  • 多关联状态组合
  • 公开状态组合

线程安全类的扩展:

  • 客户端加锁机制
  • 修改原始类源码
  • 扩展原始类
  • 代理原始类对象
作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议