临界资源: 指并发环境中多个进程/线程共享的资源.
在并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题. 例如有一份账户数据 account = 6
, 它作为一份临界资源被两个线程同时消费.
public class Test {
private volatile int account = 6;
public void consume(int amount) {
// 1. 读取账户
int currentAccount = account;
// 2. 消费账户
if (currentAccount >= amount) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
currentAccount -= amount;
System.out.println(String.format("消费了%s元", amount));
// 3. 更新账户
account = currentAccount;
}
}
public int getAccount() {
return account;
}
public static void main(String[] args) {
final Test testObj = new Test();
Thread threadA = new Thread(() -> testObj.consume(5));
Thread threadB = new Thread(() -> testObj.consume(6));
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("账户剩余: %s元", testObj.getAccount()));
}
}
运行程序后, 你会发现两个线程都消费成功了, 总共消费了 11 元. 究其原因, 是因为一个线程将临界资源置于中间状态后, 另一个线程访问了这个中间状态并基于此中间状态做了进一步的处理. 结合上面的例子, 当线程 A 以写的目的读取 account
时, 就已经将 account
置于中间状态了. 直至线程 A 完成对 account
的所有操作前, account
都处于中间状态, 而这个状态对其他线程应该是不可见的. 而上例中的线程 B 因为读取了 account
的中间状态, 并基于这个中间状态做了一系列的处理, 从而导致了数据最终不一致的问题.
总结:
- 纯粹的读操作并不会将临界资源置于中间状态;
- 以写为目的的读操作会将临界资源置于中间状态;
- 处于中间状态的临界资源不支持其他线程以写为目的的读操作, 更不支持写操作;
- 根据实际情况 (可否忍受脏读), 处于中间状态的临界资源可以支持其他线程纯粹的读操作;
正确理解临界资源的中间状态, 对于维护数据的一致性问题非常重要.