一个竞态条件是一个特殊的条件,可能发生在一个临界部分的内部(critical section)。一个临界部分是一段正在被多线程执行的代码,以及线程执行的顺序对于临界部分并发执行的结果产生影响。
当多线程执行一个临界段的结果依赖线程执行的顺序可能是不同的,这个临界段包含一个竞态条件。这个竞态条件的词条源于这个线程正在竞速通过这个临界段的暗喻,并且这个竞争的结果影响着执行这个临界段的结果。
这个可能听起来有点复杂,以至于我将会在下面的部分详细阐述关于竞态条件和临界段。
临界段(Critical Sections)
在相同的应用内部运行不止一个线程不会被他自己引起问题。当多个线程访问相同的资源问题就会出现。例如相同的内存(变量,数组,或者对象),系统(数据库,web服务)或者文件。
事实上,如果一个或者多个线程写这些资源的时候问题会出现。让多个线程读取相同的资源是安全的,只要资源不会改变。
这里有一个例子,如果多个线程同事执行可能会失败:
public class Counter { protected long count = 0; public void add(long value){ this.count = this.count + value; } }
想象下如果线程A和B正在执行相同的Counter类的实例的add方法。这里没有办法知道操作系统什么时间会在线程之间切换。add方法中的代码不会被java虚拟机作为单独的原子指令执行。而是作为一系列的更小的指令集执行,跟这个类似:
从内存中读取this.count值进入寄存器。
增加value值到寄存器。
将寄存器中的值写回内存。
观察线程A和B混合执行会发生什么:
this.count = 0; A: Reads this.count into a register (0) B: Reads this.count into a register (0) B: Adds value 2 to register B: Writes register value (2) back to memory. this.count now equals 2 A: Adds value 3 to register A: Writes register value (3) back to memory. this.count now equals 3
这两个线程想添加2和3到counter中。因此这两个线程执行完之后的值应该是5。然而,因为这两个线程执行时交叉的,因此结果以不同而结束。
在上面提到的执行顺序的例子中,两个线程都从内存中读取到0这个值。然后他们添加他们各自的值,2和3到那个值中去,然后把这个结果写回到内存中。代替5,在this.count中留下的值将会是最后的那个线程写给他的那个值。在上面的例子中是线程A,但是他也可能是线程B。
在临界段中的竞态条件
在上面的那个例子中的add方法的代码中包含了一个临界段。当多个线程执行这个临界段的时候,竞态条件就会发生了。
更正式的讲,两个线程的这种情形竞争着相同的资源,资源被访问的顺序是重要的,它被称为竞态条件。一个代码部分导致竞态条件就会被称之为临界段。
预防竞态条件
为了防止竞态条件的发生,你必须确保被执行的临界段作为一个原子指令被执行。那就意味着一旦一个单独的线程在执行它,其他的线程就不能执行它直到第一个线程已经离开了这个临界段。
竞态条件可以通过在临界段中使用线程同步的方式去避免。线程同步可以使用一个Java代码的同步锁去获取。线程同步也可以使用其他的同步概念去获取,像锁或者像java.util.concurrent.atomic.AtomicInteger的原子变量。
临界段的吞吐量
对于更小的临界段使得整个临界段的一个同步锁可能会工作。但是,对于更大的临界段,去把它分解成更小的临界段是更有意义的,使得允许多线程去执行每一个更小的临界段。整个就可能降低共享资源的竞争,以及增加整个临界段的吞吐量。
这里有一个非常简单的Java实例:
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方法是怎样往这两个sum变量中添加值得。为了预防竞态条件,在内部执行的求和有一个Java同步锁。伴随着这个实现,同时只能有一个线程可以执行这个求和。
然而,因为这两个sum变量是相互独立的,你可以把他们分离成两个分离的同步锁,像这样:
public class TwoSums { private int sum1 = 0; private int sum2 = 0; public void add(int val1, int val2){ synchronized(this){ this.sum1 += val1; } synchronized(this){ this.sum2 += val2; } } }
注意,两个线程可以同时执行这个add方法。一个线程获取到第一个同步锁,另外一个线程获取第二个同步锁。这种方式,线程之间将会等待的更少时间。
当然,这个例子是非常简单的。在真正的生活中,临界段分离的共享资源可能会更加复杂的,并且需要更多的执行顺序可能性的分析。
以上就是Java 竞态条件和临界段的内容,更多相关内容请关注PHP中文网(www.php.cn)!