如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。

有三种方式可以修复这个问题:

  • 不在线程之间共享该状态变量
  • 将状态变量改为不可变的变量
  • 在访问状态变量时使用同步

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();
    }
}