- 原文地址:Concurrency Models
- 作者: Jakob Jenkov
竞态条件 是一个可能在临界区中发生的特殊条件。 而临界区表示着,一段在并发执行和顺序执行时有着不同运行结果的代码片段。
临界区中多线程执行的结果可能会因不同的线程执行顺序,而导致不同的运行结果。 竞态条件就是源自线程同时竞争执行临界区,而竞争的结果也会影响着临界区的运行结果。
接下来再详细说说这两个概念。
临界区
在一个应用中运行多个线程,这本身不会有啥问题。 问题是出在,多个线程访问同一资源的时候。 例如:同一个内存区域(变量,数组,对象),系统资源(数据库,接口)以及文件等。
事实上,问题主要是线程对资源进行写操操作。 如果,多个线程都是执行资源读操作,那是不会有什么问题的。
下面这个例子就展示了一个临界区,当多个线程同时执行时,就会出问题:
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
想象一下,如果有两个线程,A和B,在同一个实例上进行加法操作。 而我们无法知道操作是如何调度这两个线程的执行。 虽然只有一行代码,但在JVM中
add()
方法不会以原子的方式被执行。 而是会以下面的逻辑来处理:
- 从内存读取
this.count
的值到寄存器中- 在寄存中执行加法指令操作
- 将计算结果从寄存器中写会内存
而当A,B两个线程交叉执行时,可能会得到如下的情况:
this.count = 0;
A: 读取this.count到寄存器 (0)
B: 读取this.count到寄存器 (0)
B: 在寄存器中加上2
B: 将结果2写回内存。this.count=2 (2)
A: 在寄存器中加上3
A: 将结果3写回内存。this.count=3 (3)
两个线程分别想要对计数器累加2和3。所以,执行完成时预期值应该是5。 但是,两个线程的执行过程是交叉进行的,这样就会导致结果不同了。
如果按上面的顺序执行,最后A线程的结果会直接覆盖B线程的结果。
临界区中的竞态条件
上例中,
add()
方法就是一个临界区。当多个线程执行时,触发竞态条件。
就是说,两个线程争用同一个资源,此时,执行顺序就变得很敏感了,这也就称之为竞态条件。 导致竞态条件的代码块就称为临界区。
防止竞态条件
要想防止竞态条件的发生,就要确保临界区以原子方式执行。 这就意味着,一次只有一个线程在执行,在第一个线程离开临界区之前,不会有其他线程可以进入临界区。
为了避免竞态条件,可以对临界区使用一些同步机制。线程同步可以使用Java阻塞同步,还可以使用Lock以及原子变量来进行同步。
临界区吞吐量
在上例中,对整个临界区进行同步阻塞是可以解决问题的。一般来说,对于临界区是越小越好,这样可以让每一个线程执行更小的临界区。 为了减少对共享资源的争用,这样就可以增加总体上的吞吐量。
来看看这个例子:
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
public void add(int val1, int val2){
synchronized(this){
this.sum1 += val1;
this.sum2 += val2;
}
}
}
注意
add()
方法,对两个成员变量分别进行累加和。 为了防止累加和出现竞态条件,这里使用了一个Java synchronized 阻塞。 这样一次只会有一个线程能够进行累加和的计算。
但是,要注意,两个变量的累加计算,其实是互相独立的,其实是可以分成两个同步块:
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1);
private Integer sum2Lock = new Integer(2);
public void add(int val1, int val2){
synchronized(this.sum1Lock){
this.sum1 += val1;
}
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
这样两个线程就可以并行进入
add()
方法。 一个进入第一个同步块,另一个进入第二个。 两个同步块分别在两个不同的对象上进行同步,所以两个不同的线程可以互不干扰的进入两个代码块中。 这样在add()
方法中,就可以减少线程等待的时间。
当然,这个例子太简单了。在实际工作中,将共享资源分解到多个临界区,其实是很复杂的工作。 需要分析更多情况下的执行顺序。