前言
下面简单总结学习Java并发的笔记,关于如何利用面向对象思想写好并发程序的建议。面向对象的思想和并发编程属于两个领域,但是在Java中这两个领域却可以融合到一起。在Java语言中,面向对象编程的思想能够让并发编程变得更加简单。下面将从封装共享变量、识别共享变量间的约束条件和制定并发访问策略三方面介绍如何使用面向对象思想去指导编写并发程序。
封装共享变量
在并发编程中,格外关心的一个重点便是多线程对共享变量的访问问题。我们需要控制好对共享变量的访问接口。面向对象就有一个非常好的特性:封装(将属性和实现细节封装在对象内部),外界只能通过目标对象提供的公共方法来间接访问这些内部属性。使用面向对象的这个特性,就可以非常轻松地掌控共享变量的访问路径。
利用面向对象思想编写并发程序的思路:将共享变量作为对象属性封装在内部,对所有公共方法定制并发访问策略。
例如,下面的计数器程序。计数器程序共享变量只有一个,即value
,我们把它作为Counter
类的属性,并且将两个公共方法get()
和set()
声明为同步方法,这样Counter类就成为了一个线程安全的类了。
public class Counter {
private long value;
synchronized long get(){
return value;
}
synchronized long addOne(){
return ++value;
}
}
识别共享变量之间的约束条件
识别共享变量之间的约束条件十分重要,因为这会影响到并发访问策略的定制。
下面举例说明。
在库存管理中有个合理库存的概念,库存量不能太高,也不能太低,它有一个上限和一个下限。下面使用代码说明。
在SafeVM
中,声明了两个成员变量upper
和lower
,分别代表了库存上限和下限,我们使用原子类AtomLong
来定义这两个变量。由于原子类是线程安全的,所以这两个成员变量的set()方法就不需要同步。
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);
}
// 省略其他业务代码
}
但是,我们需要注意,两个共享变量之间是有一个约束条件的:库存下限要小于库存上限。
于是我们就要加入参数校验,我们在方法setUpper()
和方法setLower()
中加入检验语句:
// 设置库存上限
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都可以同时通过校验,导致最终库存为(7,5)。
线程A执行时,下限还没有被线程 B 设置,还是 2,而 5>2;线程B执行时,上限还没有被线程 A 设置,还是 10,而 7<10。
在没有识别出库存下限要小于库存上限这个约束条件之前,我们制定的并发访问策略是利用原子类,但是这个策略,完全不能保证库存下限要小于库存上限这个约束条件。
所以,在设计阶段,我们一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。
制定并发访问策略
指定并发访问策略,从方案思想上来看,可以从以下三个方面入手:(在前一篇博客的小结中也提到过)
-
避免共享
上篇文章介绍的线程封闭技术。
-
不变模式
例如
Actor
模式,CSP
模式以及函数式编程的基础都是不变模式。 -
synchronized
同步机制和并发容器
除了以上方案思想,还有一些宏观原则需要了解。
-
优先使用成熟的工具类
使用已经设计好的工具类,避免重复造轮子。
-
迫不得已才使用低级的同步原语
低级的同步原语主要指的是
synchronized
、Lock
、Semaphore
等,虽然看上去简单,但使用起来还是要万分小心。 -
避免过早优化
先保证线程安全性,再考虑优化性能。
小结
主要是学习参考[1]时的学习总结笔记,没有加入太多自己的思考或者补充点,(◞‸◟ )积累还不够。
要好好撸起袖子加油干!( ̄^ ̄)ゞ
参考:
[1]极客时间专栏王宝令《Java并发编程实战》