关于数据竞赛定义的困惑

时间:2018-10-14 16:37:18

标签: multithreading concurrency race-condition data-race

当程序中同时有两个内存访问权限时,就会发生数据争用:

  • 定位相同的位置
  • 由两个线程同时执行
  • 不可读
  • 不是同步操作

此定义取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”。

这是我的困惑,这是我想问的两个问题:

  1. 这是一场数据竞赛吗?如果没有,为什么不呢?

  2. 如果这是一场数据竞赛,为什么提出的解决方案不起作用?

1 个答案:

答案 0 :(得分:0)

首先,线程执行的任意顺序不是数据竞争本身,即使它可能导致竞争。如果您需要同步2个或更多线程以特定顺序执行其代码,则必须使用monitors之类的等待机制。监视器是可以进行互斥(锁定)等待的结构。监视器也称为条件变量,而Java supports也称为条件变量。

现在的问题是什么是数据竞赛。当2个或更多线程同时访问同一内存位置并且某些访问是写操作时,发生数据争用。这种情况可能导致内存位置可能包含无法预测的值。

一个经典的例子。让我们有一个32位的OS和64位长的变量,例如longdouble类型。让我们拥有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_FFFFL0x7FFF_FFFF_0000_0000L。但是,如果仍然需要同步哪个线程首先执行和哪个线程第二执行,则不仅需要使用锁,还需要使用监视机制(条件变量)的等待机制。

根据article,请注意,Java保证对除longdouble之外的所有基本类型变量的原子访问。在64位平台上,甚至对longdouble的访问也应该是原子的,但是该标准似乎不能保证它。

即使标准保证了原子访问,使用锁也总是更好。锁定义了memory barriers,它们阻止了一些编译器优化,这些优化可能会使您的代码在CPU指令级别上重新排序,并且如果使用变量控制执行顺序,则会导致问题。

因此,这里的简单建议是,如果您不是并发编程专家(我也不是),并且不编写需要通过使用无锁技术来获得绝对最大性能的软件,请始终使用锁-即使访问保证具有原子访问权限的变量。

相关问题