当程序中同时有两个内存访问权限时,就会发生数据争用:
此定义取from,该定义是从研究论文中借用的,因此我们可以假定它是正确的。
现在考虑这个例子:
import java.util.concurrent.*;
class DataRace{
static boolean flag = false;
static void raiseFlag() {
flag = true;
}
public static void main(String[] args) {
ForkJoinPool.commonPool().execute(DataRace::raiseFlag);
System.out.println(flag);
}
}
据我了解,这满足了数据竞争的定义。我们有两条指令访问相同的location(flag),它们都不是读取的,都是并发的,也不是同步操作。因此,输出取决于线程的交织方式,并且可以是“ True”或“ False”。
如果我们假设这是一场数据竞赛,那么我可以在访问之前添加锁并解决此问题。但是,即使我在两个线程中都添加了锁,我们知道锁中也存在竞争状态。因此,任何线程都可以获取锁定,并且输出仍然可以为“ True”或“ False”。
这是我的困惑,这是我想问的两个问题:
这是一场数据竞赛吗?如果没有,为什么不呢?
如果这是一场数据竞赛,为什么提出的解决方案不起作用?
答案 0 :(得分:0)
首先,线程执行的任意顺序不是数据竞争本身,即使它可能导致竞争。如果您需要同步2个或更多线程以特定顺序执行其代码,则必须使用monitors之类的等待机制。监视器是可以进行互斥(锁定)和等待的结构。监视器也称为条件变量,而Java supports也称为条件变量。
现在的问题是什么是数据竞赛。当2个或更多线程同时访问同一内存位置并且某些访问是写操作时,发生数据争用。这种情况可能导致内存位置可能包含无法预测的值。
一个经典的例子。让我们有一个32位的OS和64位长的变量,例如long
或double
类型。让我们拥有long
变量。
long SharedVariable;
执行以下代码的线程1。
SharedVariable=0;
执行以下代码的线程2。
SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;
如果对此变量的访问不受锁保护,则在执行两个线程之后,SharedVariable
可以具有以下值之一。
SharedVariable==0
SharedVariable==0x7FFF_FFFF_FFFF_FFFFL
**SharedVariable==0x0000_0000_FFFF_FFFFL**
**SharedVariable==0x7FFF_FFFF_0000_0000L**
最后2个值是意外的-由数据争用引起。
这里的问题是,在32位OS上,可以保证对32位变量的访问是原子的-因此,该平台保证即使2个或更多线程正在同时访问相同的32位内存位置,也可以保证对那个内存位置的访问是原子的-只有一个线程可以访问该变量。但是因为我们有64位变量,所以在CPU级别上,写入64位长的变量将转换为2条CPU指令。因此,代码SharedVariable=0;
被翻译成如下形式:
mov SharedVariableHigh32bits,0
mov SharedVariableLow32bits,0
然后将代码SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;
转换为如下代码:
mov SharedVariableHigh32bits,0x7FFFFFFF
mov SharedVariableLow32bits,0xFFFFFFFF
没有锁,CPU可以按以下顺序执行这4条指令。
订单1。
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
结果是:0x7FFF_FFFF_FFFF_FFFFL
。
订单2。
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
结果是:0
。
订单3。
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableLow32bits,0xFFFFFFFF // T2
结果是:0x0000_0000_FFFF_FFFFL
。
订单4。
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableLow32bits,0 // T1
结果是:0x7FFF_FFFF_0000_0000L
。
因此,竞争条件导致了一个严重的问题,因为您可以获得的值完全是意外的且无效的。通过使用锁,您可以阻止它,但是仅使用锁并不能保证执行的顺序-哪个线程先执行其代码。因此,如果使用锁,则将仅获得2个执行顺序-顺序1和顺序2,而不是意外值0x0000_0000_FFFF_FFFFL
和0x7FFF_FFFF_0000_0000L
。但是,如果仍然需要同步哪个线程首先执行和哪个线程第二执行,则不仅需要使用锁,还需要使用监视机制(条件变量)的等待机制。
根据article,请注意,Java保证对除long
和double
之外的所有基本类型变量的原子访问。在64位平台上,甚至对long
和double
的访问也应该是原子的,但是该标准似乎不能保证它。
即使标准保证了原子访问,使用锁也总是更好。锁定义了memory barriers,它们阻止了一些编译器优化,这些优化可能会使您的代码在CPU指令级别上重新排序,并且如果使用变量控制执行顺序,则会导致问题。
因此,这里的简单建议是,如果您不是并发编程专家(我也不是),并且不编写需要通过使用无锁技术来获得绝对最大性能的软件,请始终使用锁-即使访问保证具有原子访问权限的变量。