如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。
有三种方式可以修复这个问题:
- 不在线程之间共享该状态变量
- 将状态变量改为不可变的变量
- 在访问状态变量时使用同步
1. 竞态条件
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气。
1.1 先检查后执行(Check-Then-Act)
先检查后执行 操作是最常见的竞态条件类型,即通过一个可能失效的观察结果来决定下一个动作。
使用 “先检查后执行” 的一种常见情况就是延迟初始化:
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
// 先检查后执行
if(instance == null)
instance = new ExpensiveObject();
return instance;
}
}
1.2 读取-修改-写入(例如递增一个计数器)
在操作中,基于对象之前的状态来定义对象状态的转换。要递增一个计数器,你必须知道它之前的值,并确保在执行更新的过程中没有其他线程会修改或使用这个值。
可以使用
java.util.concurrent.atomic
包里的原子变量
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
//递增计数器:读取-修改-写入 (竞态条件)
++count;
encodeIntoResponse(resp, factors);
}
}
2. 加锁机制
2.1 内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块 (Synchronized Block)。
同步代码块包括两部分:一个是锁的对象引用,一个是由这个锁保护的代码块。
以关键字 synchronized
来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
静态的 synchronized
方法以 Class 对象作为锁。
synchronized (lock) {
//访问或修改由锁保护的共享状态
}
每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。 线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。 获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java的内置锁相当于一个互斥体(或互斥锁),最多只有一个线程能持有这种锁。
2.2 重入
由于内置锁可重入,如果某个线程试图获得一个已经由它自己持有的锁,请求会成功。 “重入”意味着获取锁的操作的粒度是”线程”,而不是”调用”。
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。
// 如果内置锁不是可重入的,这段代码将发生死锁
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}