0%

如何用面向对象思想写好并发程序

在Java语言里,面向对象思想能够让并发编程变得更简单。
可以从封装共享变量、识别共享变量间的约束条件、制定并发访问策略三个方面下手。

封装共享变量

面向对象思想里面有一个很重要的特性是封装,封装的通俗解释就是将属性和实现细节封装在对象内部,外界对象只能通过目标对象提供的公共方法来间接访问这些内部属性。(这和门票管理模型类似,球场里的座位就是对象属性,球场入口就是对象的公共方法。)
把共享变量作为对象的属性,那对于共享变量的访问路径就是对象的公共方法,所有入口都要安排检票程序就相当于前面提到的并发访问策略。

利用面向对象思想写并发程序的思路就是:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略

实际工作中,经常要面临的情况是有很多的共享变量,如信用卡账户有卡号、姓名、身份证、信用额度、已出账单、未出账单等很多共享变量。但有些共享变量的值是不会变的,如信用卡账户的卡号、姓名、身份证。对于这些不会发生变化的共享变量,建议用final关键字来修饰

识别共享变量间的约束条件

识别共享变量间的约束条件非常重要。因为这些约束条件,决定了并发访问策略。如库存管理里面有个合理库存的概念,库存量不能太高,也不能太低,它有一个上限和一个下限。在类SafeWM中,声明了两个成员变量upper和lower,分别代表库存上限和库存下限,这两个变量用了AtomicLong这个原子类,原子类是线程安全的,所以这两个成员变量的set方法就不需要同步了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SafeWM {
//库存上限
private final AtomicLong upper = new AtomicLong(0);
//库存下限
private final AtomicLong lower = new AtomicLong(0);
//设置库存上限
void setUpper(long v) {
upper.set(v);
}
//设置库存下限
void setLower(long v) {
lower.set(v);
}
//省略其他业务代码
}

上述代码忽视了一个约束条件,就是库存下限要小于库存上限,这个约束条件如果直接加到set方法上,在setUpper()和setLower()中增加了参数校验,但存在并发问题,存在竞态条件。(代码里出现if语句的时候,就可能存在竞态条件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
//省略其他业务代码
}
  • 加锁,setUpper()和setLower()加上synchronized(this)关键字,两个set方法用同一把锁
  • 将lower和upper两个变量final修饰私有化封装到一个类中,提供一个已这两个变量为入参的公共的修改方法,方法内部添加约束条件

在没有识别出库存下限要小于库存上限这个约束条件之前,制定的并发访问策略是利用原子类,但是这个策略完全不能保证库存下限要小于库存上限这个约束条件。
在设计阶段,一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。

共享变量之间的约束条件,反映在代码里,基本上都会有if语句,一定要特别注意竞态条件。

制定并发访问策略

制定并发访问策略方案要点:

  1. 避免共享:避免共享的技术主要是利于线程本地存储以及为每个人物分配独立的线程。
  2. 不变模式:这个在Java领域应用的很少,但在其他领域有这广泛的应用,如Actor模式、CSP模式、函数式编程的基础都是不变模式。
  3. 管程及其他同步工具:Java领域万能的解决方案是管程,但是对于很多特定场景,使用Java并发包提供的读写锁、并发容器等同步工具会更好。

制定并发访问策略宏观原则:

  1. 优先使用成熟的工具类:Java SDK并发包里提供了丰富的工具类,基本上能满足日常的需要,熟悉它们、用好它们,不要再发明轮子。
  2. 迫不得已时才使用低级的同步原语:低级的同步原语主要指的是synchronized、Lock、Semaphore等。(高级同步原语:内置的支持并发的数据结构)
  3. 避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,预估性能的瓶颈是不现实的。

总结

利用面向对象思想编写并发程序,一个关键点就是利用面向对象里的封装特性,对共享变量进行封装,要避免逸出,就是共享变量逃逸到对象的外面。如构造函数里的this逸出。