- 一、封装共享变量
- 二、识别共享变量间的约束条件
- 三、制定并发访问策略
一、封装共享变量
面向对象思想里面有一个很重要的特性是封装,封装的通俗解释就是将属性和实现细节封装在对象内部,外界对象只能通过目标对象提供的公共方法来间接访问这些内部属性。
利用面向对象思想写并发程序的思路:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。
实际工作中,经常要面临的情况往往是有很多的共享变量,对于一些不会发生变化的共享变量,建议你用 final 关键字来修饰。这样既能避免并发问题,也能很明了地表明你的设计意图。
二、识别共享变量间的约束条件
识别共享变量间的约束条件非常重要。因为这些约束条件,决定了并发访问策略。
例如,库存管理里面有个合理库存的概念,库存量不能太高,也不能太低,它有一个上限和一个下限。但是容易忽视了一个约束条件,就是库存下限要小于库存上限。
public class SafeWM {
// 库存上限
private final AtomicLong upper =
new AtomicLong(0);
// 库存下限
private final AtomicLong lower =
new AtomicLong(0);
// 设置库存上限
void setUpper(long v){
// 检查参数合法性
if (v < lower.get()) {
throw new IllegalArgumentException();
}
upper.set(v);
}
// 设置库存下限
void setLower(long v){
// 检查参数合法性
if (v > upper.get()) {
throw new IllegalArgumentException();
}
lower.set(v);
}
// 省略其他业务代码
}
当你看到代码里出现 if 语句的时候,就应该立刻意识到可能存在竞态条件,导致并发问题。
在设计阶段,我们一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。
三、制定并发访问策略
制定并发访问策略,是一个非常复杂的事情。应该说整个专栏都是在尝试搞定它。不过从方案上来看,无外乎就是以下“三件事”。
- 避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
- 不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
- 管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。
除了这些方案之外,还有一些宏观的原则需要你了解。这些宏观原则,有助于你写出“健壮”的并发程序。这些原则主要有以下三条。
- 优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议你熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
- 迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
- 避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。
课后思考
本期示例代码中,类 SafeWM 不满足库存下限要小于库存上限这个约束条件,那你来试试修改一下,让它能够在并发条件下满足库存下限要小于库存上限这个约束条件。
答:
- setUpper() 跟 setLower() 都加上 "synchronized" 关键字。不要太在意性能,老师都说了,避免过早优化。
如果性能有问题,可以把 lower 跟 upper 两个变量封装到一个类中,例如
public class Boundary { private final lower;
private final upper;
public Boundary(long lower, long upper) {
if(lower >= upper) {
// throw exception
}
this.lower = lower;
this.upper = upper;
}
}移除 SafeVM 的 setUpper() 跟 setLower() 方法,并增入 setBoundary(Boundary boundary) 方法。