如何用面对对象思想写好并发程序
封装共享变量
将共享变量作为对象属性封装在内部, 对所有公共方法制定并发访问策略
且, 对于这些不会发生变化的共享变量,建议用 final 关键字来修饰
**
示例: Counter类就是一个线程安全的类
public class Counter {
private long value;
synchronized long get(){
return value;
}
synchronized long addOne(){
return ++value;
}
}
识别共享变量间的约束条件
在设计阶段, 一定要识别出所有共享变量之间的约束条件, 如果约束条件识别不足, 很可能导致制定的并发访问策略南辕北辙
示例: 原子类不能保证不能保证库存下限要小于库存上限这个约束条件
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);
}
// 省略其他业务代码
}
制定并发访问策略
- 避免共享: 使用线程本地存储以及为每个任务分配独立的线程
- 不变模式: java中应用较少, 但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
- 管程及其他同步工具: 使用 Java 并发包提供的读写锁、并发容器等同步工具
宏观原则:
1.优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议你熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
2.迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
3.避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。
补充答疑
Integer 和 String 和Boolean类型的对象不适合做锁
锁,应是私有的、不可变的、不可重用的。
Integer会缓存-128~127这个范围内的数值,String对象同样会缓存字符串常量到字符串常量池,可供重复使用,所以不能用来用作锁对象,此外还有Boolean可能被JVM重用
示例: 下面代码线程不安全
class Account {
// 账户余额
private Integer balance;
// 账户密码
private String password;
// 取款
void withdraw(Integer amt) {
synchronized(balance) {
if (this.balance > amt){
this.balance -= amt;
}
}
}
// 更改密码
void updatePassword(String pw){
synchronized(password) {
this.password = pw;
}
}
}
规范的锁示例:
// 普通对象锁
private final Object
lock = new Object();
// 静态对象锁
private static final Object
lock = new Object();
最佳线程 =2 * CPU 的核数 + 1 ?
从理论上来讲,这个经验值一定是靠不住的。但是经验值对于很多“I/O 耗时 / CPU 耗时”不太容易确定的系统来说,却是一个很好到初始值。
实际工作中面临的系统, “I/O 耗时 / CPU 耗时”往往都大于 1,所以基本上都是在这个初始值的基础上增加。增加的过程中,应关注线程数是如何影响吞吐量和延迟的。一般来讲,随着线程数的增加,吞吐量会增加,延迟也会缓慢增加;但是当线程数增加到一定程度,吞吐量就会开始下降,延迟会迅速增加。这个时候基本上就是线程能够设置的最大值了。