5

我遇到了下面这个声称是线程安全的 Java 类的示例。谁能解释一下它如何是线程安全的?我可以清楚地看到,类中​​的最后一个方法没有受到任何读取器线程的并发访问的保护。或者,我在这里遗漏了什么?

public class Account {
    private Lock lock = new ReentrantLock();
    private int value = 0;
    public void increment() {
       lock.lock();
       value++;
       lock.unlock();
    }
    public void decrement() {
       lock.lock();
       value--;
       lock.unlock();
    }
    public int getValue() {
       return value;
    }
}
4

6 回答 6

6

代码不是线程安全的。

假设一个线程调用decrement,然后第二个线程调用getValue. 发生什么了?

问题是和之间没有“发生在之前”的关系。这意味着无法保证调用会看到. 实际上,可能会“错过”不确定的和调用序列的结果。decrementgetValuegetValuedecrementgetValueincrementdecrement

实际上,除非我们看到使用Account该类的代码,否则线程安全的问题是不明确的。程序的线程安全1的传统概念是关于代码是否正确运行,而与线程相关的不确定性无关。在这种情况下,我们没有关于什么是“正确”行为的规范,或者确实没有要测试或检查的可执行程序。

但我对代码2的解读是,有一个隐含的API 要求/正确性标准getValue返回帐户的当前值。如果有多个线程,则无法保证,因此该类不是线程安全的。

相关链接:


1 - @CKing 的答案中的 Concurrency in Practice 引用也通过在定义中提到“无效状态”来吸引“正确性”的概念。但是,内存模型的 JLS 部分没有指定线程安全。相反,他们谈论的是“格式良好的处决”。

2 - 下面的 OP 评论支持此读数。但是,如果您不接受这个要求是真实的(例如,因为它没有明确说明),那么另一方面是“帐户”抽象的行为取决于Account类之外的代码......这使得这是一个“泄漏的抽象”。

于 2015-07-10T15:11:50.650 回答
2

这纯粹不是线程安全的,因为无法保证编译器如何重新排序。由于 value 不是 volatile 这里是您的经典示例:

while(account.getValue() != 0){

}

这可以吊起来看起来像

while(true){
  if(account.getValue() != 0){

  } else {
      break;
  }
}

我可以想象编译器乐趣的其他排列可能会导致它巧妙地失败。但是getValue通过多个线程访问它可能会导致失败。

于 2015-07-10T15:56:27.743 回答
2

这里有几个不同的问题:

increment()问:如果多个线程对and进行重叠调用decrement(),然后它们停止,然后经过足够长的时间而没有线程调用increment()or decrement(),getValue() 会返回正确的数字吗?

答:是的。递增和递减方法中的锁定确保每个递增和递减操作都将自动发生。他们不能互相干扰。


问:多长时间才够用

答:这很难说。Java 语言规范不保证调用 getValue() 的线程将永远看到其他线程写入的最新值,因为 getValue() 访问该值时根本没有任何同步。

如果您更改 getValue() 以锁定和解锁相同的锁定对象,或者如果您将 count 声明为volatile,那么零时间就足够了。


问:调用可以getValue()返回无效值吗?

A: 不可以,它只能返回初始值,或者完全increment()调用的结果,或者完全decrement()操作的结果。

但是,这样做的原因与 lock 无关。锁不会阻止任何线程调用getValue(),而其他线程正在增加或减少值。

阻止 getValue() 返回完全无效值的原因是 value 是 an int,并且 JLS 保证int变量的更新和读取始终是原子的。

于 2015-07-10T16:06:03.350 回答
1

简短的回答:

根据定义,Account它是一个线程安全的类,即使geValue方法没有被保护

长答案

在实践中,从Java Concurrency来看,在以下情况下,一个类被称为线程安全的:

在线程安全类的实例上顺序或并发执行的任何操作集都不会导致实例处于无效状态。

由于该getValue方法不会导致Account类在任何给定时间处于无效状态,因此您的类被称为线程安全的。

Collections#synchronizedCollection的文档引起了这种情绪的共鸣:

返回由指定集合支持的同步(线程安全)集合。为了保证串行访问,对后备集合的所有访问都是通过返回的集合完成的,这一点至关重要。当迭代它时,用户必须手动同步返回的集合:

 Collection c = Collections.synchronizedCollection(myCollection);
 ...   
  synchronized (c) {
  Iterator i = c.iterator(); // Must be in the synchronized block
  while (i.hasNext())
     foo(i.next());   
  }

请注意文档如何说集合(它是在类中命名的内部类的对象SynchronizedCollectionCollections线程安全的,并且在迭代集合时要求客户端代码保护集合。事实上,中的iterator方法SynchronizedCollection不是synchronized。这与您的示例非常相似,该示例Account是线程安全的,但客户端代码在调用时仍需要确保原子性getValue

于 2015-07-10T15:17:03.110 回答
0

它是完全线程安全的。

没有人可以同时递增和递减value,因此您不会丢失或获得错误的计数。

getValue() 随着时间的推移会返回不同值的事实无论如何都会发生:同时性是不相关的。

于 2015-07-10T15:03:22.837 回答
0

您不必保护 getValue。同时从多个线程访问它不会导致任何负面影响。无论何时或从多少个线程调用此方法,对象状态都不会变为无效(因为它不会改变)。

话虽如此 - 您可以编写使用此类的非线程安全代码。

例如像

if (acc.getValue()>0) acc.decrement();

具有潜在危险,因为它可能导致竞争条件。为什么?

假设您有一个业务规则“从不低于 0”,您的当前值为 1,并且有两个线程执行此代码。他们有可能按以下顺序执行此操作:

  1. 线程 1 检查 acc.getValue 是否 >0。是的!

  2. 线程 2 acc.getValue > 0。是的!

  3. 线程 1 调用递减。值为 0

  4. 线程 2 调用递减。值现在是 -1

发生了什么?每个功能都确保它不会低于零,但他们一起设法做到了。这称为竞争条件。

为避免这种情况,您不能保护基本操作,而必须保护任何必须不间断执行的代码。

所以,这个类是线程安全的,但仅用于非常有限的用途。

于 2015-07-10T15:09:54.253 回答