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

封装共享变量

将共享变量作为对象属性封装在内部, 对所有公共方法制定并发访问策略
且, 对于这些不会发生变化的共享变量,建议用 final 关键字来修饰
**
示例: Counter类就是一个线程安全的类

  1. public class Counter {
  2. private long value;
  3. synchronized long get(){
  4. return value;
  5. }
  6. synchronized long addOne(){
  7. return ++value;
  8. }
  9. }

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

在设计阶段, 一定要识别出所有共享变量之间的约束条件, 如果约束条件识别不足, 很可能导致制定的并发访问策略南辕北辙

示例: 原子类不能保证不能保证库存下限要小于库存上限这个约束条件

  1. public class SafeWM {
  2. // 库存上限
  3. private final AtomicLong upper =
  4. new AtomicLong(0);
  5. // 库存下限
  6. private final AtomicLong lower =
  7. new AtomicLong(0);
  8. // 设置库存上限
  9. void setUpper(long v){
  10. // 检查参数合法性
  11. if (v < lower.get()) {
  12. throw new IllegalArgumentException();
  13. }
  14. upper.set(v);
  15. }
  16. // 设置库存下限
  17. void setLower(long v){
  18. // 检查参数合法性
  19. if (v > upper.get()) {
  20. throw new IllegalArgumentException();
  21. }
  22. lower.set(v);
  23. }
  24. // 省略其他业务代码
  25. }

制定并发访问策略

  • 避免共享: 使用线程本地存储以及为每个任务分配独立的线程
  • 不变模式: java中应用较少, 但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
  • 管程及其他同步工具: 使用 Java 并发包提供的读写锁、并发容器等同步工具

宏观原则:
1.优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议你熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
2.迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
3.避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。

补充答疑

Integer 和 String 和Boolean类型的对象不适合做锁

锁,应是私有的、不可变的、不可重用的。
Integer会缓存-128~127这个范围内的数值,String对象同样会缓存字符串常量到字符串常量池,可供重复使用,所以不能用来用作锁对象,此外还有Boolean可能被JVM重用

示例: 下面代码线程不安全

  1. class Account {
  2. // 账户余额
  3. private Integer balance;
  4. // 账户密码
  5. private String password;
  6. // 取款
  7. void withdraw(Integer amt) {
  8. synchronized(balance) {
  9. if (this.balance > amt){
  10. this.balance -= amt;
  11. }
  12. }
  13. }
  14. // 更改密码
  15. void updatePassword(String pw){
  16. synchronized(password) {
  17. this.password = pw;
  18. }
  19. }
  20. }

规范的锁示例:

  1. // 普通对象锁
  2. private final Object
  3. lock = new Object();
  4. // 静态对象锁
  5. private static final Object
  6. lock = new Object();

最佳线程 =2 * CPU 的核数 + 1 ?

从理论上来讲,这个经验值一定是靠不住的。但是经验值对于很多“I/O 耗时 / CPU 耗时”不太容易确定的系统来说,却是一个很好到初始值。

实际工作中面临的系统, “I/O 耗时 / CPU 耗时”往往都大于 1,所以基本上都是在这个初始值的基础上增加。增加的过程中,应关注线程数是如何影响吞吐量和延迟的。一般来讲,随着线程数的增加,吞吐量会增加,延迟也会缓慢增加;但是当线程数增加到一定程度,吞吐量就会开始下降,延迟会迅速增加。这个时候基本上就是线程能够设置的最大值了。