我遇到了一个声称是线程安全的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;
}
}
答案 0 :(得分:6)
代码不是线程安全的。
假设一个线程调用decrement
,然后第二个线程调用getValue
。会发生什么?
问题是decrement
和getValue
之间没有“发生在关系之前。这意味着无法保证getValue
调用会看到decrement
的结果。实际上,getValue
可能会“错过”increment
和decrement
来电的无限期序列的结果。
实际上,除非我们看到使用Account
类的代码,否则线程安全问题是不明确的。程序的线程安全性 1 的传统概念是关于代码是否正确 ,而不管线程相关的非确定性。在这种情况下,我们没有说明“正确”行为是什么,或者实际上是一个可测试或检查的可执行程序。
但是我对代码 2 的阅读是有一个隐含的 API要求/正确性标准,getValue
返回当前帐户的价值。如果有多个线程,则无法保证这一点,因此该类不是线程安全的。
相关链接:
1 - @ CKing的答案中的并发实践引用也通过在定义中提及“无效状态”来吸引“正确性”的概念。但是,内存模型上的JLS部分未指定线程安全性。相反,他们谈论“结构良好的执行”。
2 - OP的评论支持此读数。但是,如果你不接受这个要求是真实的(例如因为没有明确说明),那么另一方面是“帐户”抽象的行为取决于Account
类之外的代码的行为......这使得这是一个“漏洞的抽象”。
答案 1 :(得分:2)
这不是线程安全,纯粹是因为无法保证编译器如何重新排序。由于价值不易变,这是典型的例子:
while(account.getValue() != 0){
}
这可以提升为
while(true){
if(account.getValue() != 0){
} else {
break;
}
}
我可以想象编译器乐趣的其他排列可能导致这种巧妙的失败。但是,通过多个线程访问此getValue
可能会导致失败。
答案 2 :(得分:2)
这里有几个不同的问题:
问:如果多个线程重复调用increment()
和decrement()
,然后停止,然后足够的时间通过而没有线程调用increment()
或者decrement()
,getValue()会返回正确的数字吗?
答:是的。增量和减量方法中的锁定确保每个递增和递减操作将以原子方式发生。他们不能互相干扰。
问:足够的时间?
答:很难说。 Java语言规范不保证调用getValue()的线程永远看到其他线程写的最新值,因为getValue()访问该值而没有任何同步。如果更改getValue()以锁定和解锁相同的锁定对象,或者如果您将计数声明为volatile
,那么零时间就足够了。
问:拨打getValue()
是否可以返回无效的值?
答:不,它只能返回初始值,或完整increment()
调用的结果或完整decrement()
操作的结果。
但是,其原因是与锁无关。锁定 not 阻止任何线程调用getValue()
,而其他线程正在递增或递减值。
阻止getValue()返回完全无效的值的是该值为int
,并且JLS保证int
变量的更新和读取始终是原子的。
答案 3 :(得分:1)
答案简短:
根据定义,Account
是线程安全的类,即使geValue
方法不是保护
答案很长
从实践中的Java并发在以下情况下,一个类被认为是线程安全的:
没有按顺序或同时执行的操作集 线程安全类的实例可以导致实例在 无效的状态。
由于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()); }
请注意文档如何说集合(SynchronizedCollection
类中名为Collections
的内部类的对象)是线程安全的,然后询问客户端代码在迭代时保护集合。事实上,iterator
中的SynchronizedCollection
方法不是synchronized
。这与Account
是线程安全的示例非常相似,但客户端代码在调用getValue
时仍需要确保原子性。
答案 4 :(得分:0)
它完全是线程安全的。
没有人可以同时增加和减少value
,这样你就不会输错或计算错误。
getValue()
将随时间返回不同值的事实无论如何都会发生:同时性无关紧要。
答案 5 :(得分:0)
您无需保护getValue。同时从多个线程访问它不会导致任何负面影响。无论何时或多少线程调用此methid,对象状态都不会变为无效(因为它不会更改)。
话虽如此 - 您可以编写一个使用此类的非线程安全代码。
例如
if (acc.getValue()>0) acc.decrement();
有潜在危险,因为它可能导致竞争条件。为什么?
假设您有一个业务规则“永不递减低于0”,您当前的值为1,并且有两个线程执行此代码。他们有可能按以下顺序执行此操作:
线程1检查acc.getValue是否> 0。是!
acc.getValue为> 0的主题2。是!
线程1调用递减。值为0
线程2调用减量。值现在为-1
发生什么事了?每个功能都确保它不会低于零,但他们一起设法做到了。这称为竞争条件。
为避免这种情况,您不能保护基本操作,而是保护必须不间断执行的任何代码段。
因此,这个类是线程安全的,但仅限于非常有限的用途。