封装共享变量

  1. 面向对象思想里面有一个很重要的特性:_封装_
  2. 封装:将属性实现细节封装在对象内部,外部对象只能通过目标对象的公共方法间接访问目标对象的内部属性
  3. 利用面向对象思想写并发程序的思路:将共享变量作为对象属性封装在内部,对所有公共方法制定_并发访问策略_

Counter

value为共享变量,作为Counter的实例属性,将get()和addOne()声明为synchronized方法,Counter就是一个线程安全的类

1
2
3
4
5
6
7
8
9
10
11
public class Counter {
private long value;

public synchronized long get() {
return value;
}

public synchronized long addOne() {
return ++value;
}
}

不可变的共享变量

  1. 实际场景中,会有很多共享变量,如银行账户有卡号、姓名、身份证、信用额度等,其中卡号、姓名和身份证是不会变的
  2. 对于不可变的共享变量,可以使用final关键字修饰,从而_避免并发问题_

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

  1. 共享变量间的约束条件决定了_并发访问策略_
  2. 场景:库存管理中有个合理库存的概念,即库存有一个上限和一个下限

忽略约束条件

下面代码忽略了约束条件:即库存下限要小于库存上限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SafeWM {
// 库存下限
private final AtomicLong lower = new AtomicLong(0);
// 库存上限
private final AtomicLong upper = new AtomicLong(0);

// 设置库存下限
public void setLower(long v) {
lower.set(v);
}

// 设置库存上限
public void setUpper(long v) {
upper.set(v);
}
}

存在竟态条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SafeWM {
// 库存下限
private final AtomicLong lower = new AtomicLong(0);
// 库存上限
private final AtomicLong upper = new AtomicLong(0);

// 设置库存下限
public void setLower(long v) {
// 检验参数合法性
if (v > upper.get()) {
throw new IllegalArgumentException();
}
lower.set(v);
}

// 设置库存上限
public void setUpper(long v) {
// 检验参数合法性
if (v < lower.get()) {
throw new IllegalArgumentException();
}
upper.set(v);
}
}
  1. 上述代码存在_竟态条件_
  2. 假设库存的下限和上限分别为2和10,线程A调用setUpper(5),线程B调用setLower(7)
  3. 线程A和线程B并发执行的结果可能是(7,5),不符合约束条件

使用管程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SafeWM {
// 库存下限
private final AtomicLong lower = new AtomicLong(0);
// 库存上限
private final AtomicLong upper = new AtomicLong(0);

// 设置库存下限
public synchronized void setLower(long v) {
// 检验参数合法性
if (v > upper.get()) {
throw new IllegalArgumentException();
}
lower.set(v);
}

// 设置库存上限
public synchronized void setUpper(long v) {
// 检验参数合法性
if (v < lower.get()) {
throw new IllegalArgumentException();
}
upper.set(v);
}
}

制定并发访问策略

  1. 避免共享:ThreadLocal + 为每个任务分配独立的线程
  2. 不变模式:Java领域应用得很少
  3. 管程和其它同步工具:管程是万能解决方案,但针对特定场景,使用JUC提供的读写锁、并发容器等同步工具性能会更好

宏观原则

  1. 优先使用成熟的工具类(JUC)
  2. 尽量少使用低级的同步原语(synchronized、Lock、Semaphore)
  3. 避免过早优化

参考资料

Java并发编程实战