多线程编程核心技术(十)如何用面向对象思想写好并发程序?
如果在设计的时候就按照单线程的思想进行设计,敲出的代码可能上线之后就会有很多问题,如何利用思想设计出符合线程安全的代码呢?
理论上面向对象编程和并发编程是没有关系的,但是面向对象编程可以更好的提升设计并发时候的逻辑
可以从三个角度上去思考,封装共享变量,识别共享变量之间的约束条件,制定并发访问策略。
封装共享变量
封装是面向对象思想中很重要的一点,把内部和细节包含在类内部,仅仅暴露出方法来对外。
那对于并发编程就可以把共享变量作为对象属性封装在内部,通过安全的处理,让外部进行访问。
public class Counter { private long value; synchronized long get(){ return value; } synchronized long addOne(){ return ++value; } }
例如这个代码通过加锁的方式来实现代码的安全。但在外部调用的时候只需要counter.xxx,这种方式就可以,不需要知道内部的执行。
第二个是巧用final关键字,final修饰的表示不可变,很多共享的值是不会变的,那么就可以使用final进行修饰
fianl的禁止重排序前提是构造函数里面没有this逃逸,他只保证final变量不会重排序到构造函数之外。并不保证逃逸。
public class demo11 { private final int Max = 9999; private volatile int num = 0; public void domMain(){ if (num<Max){ //代码逻辑 }else { //代码逻辑 } } }
识别共享变量之间的约束条件
例如现在有个需求,实现一个货物销售的功能,那么需要一个上限和下限,其中上限是需要大于下限的。
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); } // 省略其他业务代码 }
我们假设库存的下限和上限分别是 (2,10),线程 A 调用 setUpper(5) 将上限设置为 5,线程 B 调用 setLower(7) 将下限设置为 7,如果线程 A 和线程 B 完全同时执行,你会发现线程 A 能够通过参数校验,因为这个时候,下限还没有被线程 B 设置,还是 2,而 5>2;线程 B 也能够通过参数校验,因为这个时候,上限还没有被线程 A 设置,还是 10,而 7<10。当线程 A 和线程 B 都通过参数校验后,就把库存的下限和上限设置成 (7, 5) 了,显然此时的结果是不符合库存下限要小于库存上限这个约束条件的。
其实当你看到代码里出现 if 语句的时候,就应该立刻意识到可能存在竞态条件。
制定并发访问策略
避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。
原则:
优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议你熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。
在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的