何时需要锁定

时间:2014-02-15 16:45:30

标签: java multithreading concurrency consistency

好吧,我知道这可能听起来很愚蠢(而且我很害怕),但我对自己给出的答案并不完全满意,所以我觉得值得在这里问一下。 我正在处理关于并发的练习(在Java中),就像这样

  

给定已解决的数独图表,使用同时运行的固定数量的线程确定图表是否已正确解析,即不会违反规范规则(数字必须出现在其行内,其列,它只阻止一次)。

现在我的问题是:由于线程只需要执行“读取”,从图表中收集信息并在其他地方详细说明,它们不能在不担心并发的情况下工作吗?图表的状态总是一致的,因为没有执行“写入”,因此它永远不会改变。

当且仅当存在资源一致性丢失的风险时,是否需要锁定/同步块/同步方法?换句话说,我是否以正确的方式理解并发性?

7 个答案:

答案 0 :(得分:9)

这是一个相当微妙的问题,根本不是愚蠢的。

同时读取数据结构的多个线程可能会在没有同步的情况下执行此操作,仅在数据结构已安全发布时。这是内存可见性问题,而不是计时问题或竞争条件。

见Goetz等第3.5节。 al。, Java Concurrency In Practice ,以进一步讨论安全发布的概念。关于“有效不可变对象”的第3.5.4节似乎适用于此,因为董事会在某一点变得有效不可变,因为它在达到解决状态后永远不会被写入。

简而言之,编写者线程和读者线程必须执行一些内存协调活动,以确保读者线程对所写内容具有一致的视图。例如,编写器线程可以编写数独板,然后在持有锁时,在静态字段中存储对板的引用。然后读取线程可以加载该引用,同时保持锁定。一旦他们完成了这项工作,他们就可以确保所有以前对电路板的写入都是可见且一致的。之后,读取器线程可以自由访问板结构,无需进一步同步。

还有其他方法可以协调内存可见性,例如对易失性变量或AtomicReference进行写入/读取。使用更高级别的并发结构(如锁存器或障碍)或向ExecutorService提交任务,也将提供内存可见性保证。

<强>更新

根据与Donal Fellows的评论中的交换,我还应指出安全发布要求在从读者线程中获取结果时也适用。也就是说,一旦读取器线程中的一个具有来自其计算部分的结果,它就需要在某处发布该结果,以便它可以与其他读取器线程的结果组合。可以像以前一样使用相同的技术,例如在共享数据结构上锁定/同步,挥发性等。但是,这通常是不必要的,因为结果可以从{{1}返回的Future获得。 }或ExecutorService.submit。这些构造自动处理安全发布要求,因此应用程序不必处理同步。

答案 1 :(得分:2)

在我看来,你的理解是正确的。只有当任何线程正在写入数据时,才会发生数据损坏。

如果您100%确定没有线程在写,那么跳过同步和锁定是安全的......

编辑:在这些情况下跳过锁定是最好的做法! :)

答案 2 :(得分:1)

如果文件是只读的,则无需同步文件。基本锁定应用于关键部分。关键部分是,不同的线程同时访问共享内存。 由于同步使程序变慢,因为没有多个线程同时访问,所以最好不要在只读文件的情况下使用锁。

答案 3 :(得分:1)

想象一下,你有很多工作需要完成(检查9行,9列,9个块)。如果你想让线程完成这27个单元的工作,如果你想在没有双重工作的情况下完成工作,那么线程就需要同步。另一方面,如果您希望拥有可以执行另一个线程完成的工作单元的线程,那么您不需要同步线程。

答案 4 :(得分:0)

情况,其中Thread1写入一些数据,然后一堆线程需要读取此数据,如果正确完成则不需要锁定。正确地说,我的意思是你的SUDOKU板是一个不可变的对象,我的意思是不可变对象:

  • 施工后状态无法修改
  • 状态实际上并未通过某种反射暗魔法修改
  • 所有字段都是最终字段
  • 'this'引用在构造期间不会逃脱(如果在构造期间你可以在MyClass.instnce = this行上做某事)。

如果将此对象传递给工作线程,则可以使用。如果您的对象不满足所有这些条件,您仍然可能遇到并发问题,在大多数情况下,这是由于JVM可能会随意重新排序语句(出于性能原因),并且可能会以这种方式重新排序这些语句工作线程在数独板构建之前启动。

这是一篇关于immutable objects的非常好的文章。

答案 5 :(得分:0)

<强>抽象

为了保证线程能够观察写入主存储器的影响,写入必须读取之前发生。如果在不同的线程中进行写入和读取,则需要执行同步操作。规范定义了许多不同类型的同步动作。其中一个操作是执行synchronized语句,但存在替代方案。

<强>详情

Java语言规范writes

  

发生在之前的关系可以订购两个动作。如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前可见并在第二个之前被命令。

  

更具体地说,如果两个动作共享一个发生在之前的关系,那么它们不一定必须按照那个顺序发生在它们不与之共享的任何代码中。例如,在另一个线程中读取的数据争用中的一个线程中的写入可能看起来与这些读取无关。

在您的情况下,您希望读取线程解决正确的数独。也就是说,数据对象的初始化必须对读取线程可见,因此初始化必须发生在从数独读取读取线程之前。

规范定义发生在之前,如下所示:

  

如果我们有x和y两个操作,我们会写hb(x, y)来表示x 发生在 y之前。

     
      
  • 如果xy是同一个帖子的操作,并且x按照程序顺序排在y之前,那么hb(x, y)

  •   
  • 从对象的构造函数结尾到该对象的终结符(第12.6节)的开头有一个发生前的边缘。

  •   
  • 如果动作x与后续动作y同步,那么我们也有hb(x,y)。

  •   
  • 如果是hb(x,y)和hb(y,z),那么hb(x,z)。

  •   

由于读取发生在与写入不同的线程中(而不是在终结器中),因此我们需要一个同步操作来确定写入发生在读取之前。规范提供了以下详尽的同步操作列表:

  
      
  • 监视器m上的解锁操作与m上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。

  •   
  • 对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。

  •   
  • 启动线程的动作与它启动的线程中的第一个动作同步。

  •   
  • 对每个变量写入默认值(零,false或null)与每个线程中的第一个操作同步。 (虽然在分配包含变量的对象之前向变量写入默认值可能看起来有点奇怪,但概念上每个对象都是在程序开头使用其默认初始化值创建的。)

  •   
  • 线程T1中的最终操作与另一个检测到T1已终止的线程T2中的任何操作同步(T2可以通过调用T1.isAlive()T1.join()来完成此操作)

  •   
  • 如果线程T1中断线程T2,则T1的中断与任何其他线程(包括T2)确定T2已被中断的任何点同步(通过抛出InterruptedException或通过调用Thread.interrupted或Thread .isInterrupted)。

  •   

您可以选择其中任何一种方法来建立先发生的事件。实际上,在完全构建数独之后启动读取线程可能是最简单的方法。

答案 6 :(得分:-2)

从我的角度来看,如果您编写并且由于网络延迟或大量处理开销而导致需要很长时间才能完成锁定。 否则,保持锁定是非常安全的。