部分线程安全是否使 Java class 线程安全?
Does partial thread-safety make a Java class thread-safe?
我遇到了下面的 Java class 示例,它声称是线程安全的。谁能解释一下它是如何线程安全的?我可以清楚地看到 class 中的最后一个方法没有被保护以防止任何 reader 线程的并发访问。或者,我在这里遗漏了什么吗?
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;
}
}
它是完全线程安全的。
没有人可以同时递增和递减value
,因此您不会错误地失去或获得计数。
事实上 getValue()
将 return 随时间变化的不同值无论如何都会发生:同时性无关紧要。
您不必保护 getValue。同时从多个线程访问它不会导致任何负面影响。无论您何时或从多少线程调用此方法,对象状态都不会变为无效(因为它不会更改)。
话虽如此 - 您可以编写一个使用此 class.
的非线程安全代码
例如
if (acc.getValue()>0) acc.decrement();
具有潜在危险,因为它可能导致竞争条件。为什么?
假设您有一个业务规则"never decrement below 0",您的当前值为 1,并且有两个线程在执行此代码。他们有可能会按以下顺序进行:
线程 1 检查 acc.getValue 是否 >0。是的!
线程 2 acc.getValue >0。是的!
线程1调用递减。值为 0
线程 2 调用递减。现在的值为 -1
发生什么事了?每个功能都确保它不会低于零,但他们一起设法做到了这一点。这称为竞争条件。
为避免这种情况,您不能保护基本操作,而是保护任何必须不间断执行的代码片段。
因此,这个 class 是线程安全的,但用途非常有限。
代码不是线程安全的。
假设一个线程调用 decrement
,然后第二个线程调用 getValue
。会发生什么?
问题是 decrement
和 getValue
之间没有 "happens before" 关系。这意味着无法保证 getValue
调用会看到 decrement
的结果。事实上,getValue
可以 "miss" 无限序列的 increment
和 decrement
调用的结果。
实际上,除非我们看到使用 Account
class 的代码,否则线程安全问题是不明确的。程序的线程安全1 的传统概念是关于代码是否正确 行为,而与线程相关的不确定性无关。在这种情况下,我们没有关于什么是 "correct" 行为的规范,或者实际上没有要测试或检查的可执行程序。
但是我对代码2的阅读是有一个暗示API要求/正确性标准getValue
returns 账户的 当前 值。如果有多个线程则无法保证,因此 class 不是线程安全的。
相关链接:
1 - @CKing 的回答中的并发实践引用也通过在定义中提及 "invalid state" 来吸引 "correctness" 的概念。但是,关于内存模型的 JLS 部分没有指定线程安全。相反,他们谈论 "well-formed executions".
2 - OP 在下面的评论支持此阅读。但是,如果您不接受这个要求是真实的(例如因为它没有明确说明),那么另一方面是 "account" 抽象的行为取决于 [=19= 之外的代码如何] class ...这使它成为 "leaky abstraction".
简答:
根据定义,Account
是 线程安全的 class 即使 geValue
方法不受 保护
长答案
来自Java 实践中的并发性 class 在以下情况下被认为是线程安全的:
No set of operations performed sequentially or concurrently on
instances of a thread-safe class can cause an instance to be in an
invalid state.
由于 getValue
方法不会导致 Account
class 在任何给定时间处于无效状态,因此您的 class 被认为是线程安全的。
Collections#synchronizedCollection 的文档引起了这种情绪的共鸣:
Returns a synchronized (thread-safe) collection backed by the
specified collection. In order to guarantee serial access, it is
critical that all access to the backing collection is accomplished
through the returned collection. It is imperative that the user
manually synchronize on the returned collection when iterating over
it:
Collection c = Collections.synchronizedCollection(myCollection);
...
synchronized (c) {
Iterator i = c.iterator(); // Must be in the synchronized block
while (i.hasNext())
foo(i.next());
}
注意文档是如何说集合(它是 Collections
class 中名为 SynchronizedCollection
的内部 class 的对象)是 thread-safe 并要求客户端代码在遍历集合时保护集合。事实上,SynchronizedCollection
中的iterator
方法不是synchronized
。这与您的示例非常相似,其中 Account
是线程安全的,但客户端代码在调用 getValue
.
时仍需要确保原子性
这不是线程安全的,纯粹是因为无法保证编译器如何重新排序。由于值不是易变的,这里是您的经典示例:
while(account.getValue() != 0){
}
这可以吊起来看起来像
while(true){
if(account.getValue() != 0){
} else {
break;
}
}
我可以想象还有其他编译器乐趣的排列可能会导致它巧妙地失败。但是通过多线程访问此 getValue
可能会导致失败。
这里有几个不同的问题:
问:如果多个线程重叠调用 increment()
和 decrement()
,然后它们停止,然后 足够的时间 没有线程调用increment()
或 decrement()
,getValue() return 会是正确的数字吗?
答:是的。递增和递减方法中的锁定确保每个递增和递减操作将自动发生。他们不能互相干扰。
问:多长时间足够?
A:这很难说。 Java 语言规范不保证调用 getValue() 的线程将 永远 看到其他线程写入的最新值,因为 getValue() 访问该值时没有任何同步全部.
如果您更改 getValue() 以锁定和解锁同一个锁定对象,或者如果您将计数声明为 volatile
,那么零时间就足够了。
问:可以调用 getValue()
return 一个 无效的 值吗?
A: 不,它只能 return 初始值,或完整 increment()
调用的结果或完整 decrement()
操作的结果。
但是,这样做的原因与锁无关。该锁 不会 阻止任何线程调用 getValue()
而其他线程正在递增或递减值。
阻止 getValue() return 获取完全无效值的原因是该值是一个 int
,并且 JLS 保证 int
变量的更新和读取是总是原子的。
我遇到了下面的 Java class 示例,它声称是线程安全的。谁能解释一下它是如何线程安全的?我可以清楚地看到 class 中的最后一个方法没有被保护以防止任何 reader 线程的并发访问。或者,我在这里遗漏了什么吗?
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;
}
}
它是完全线程安全的。
没有人可以同时递增和递减value
,因此您不会错误地失去或获得计数。
事实上 getValue()
将 return 随时间变化的不同值无论如何都会发生:同时性无关紧要。
您不必保护 getValue。同时从多个线程访问它不会导致任何负面影响。无论您何时或从多少线程调用此方法,对象状态都不会变为无效(因为它不会更改)。
话虽如此 - 您可以编写一个使用此 class.
的非线程安全代码例如
if (acc.getValue()>0) acc.decrement();
具有潜在危险,因为它可能导致竞争条件。为什么?
假设您有一个业务规则"never decrement below 0",您的当前值为 1,并且有两个线程在执行此代码。他们有可能会按以下顺序进行:
线程 1 检查 acc.getValue 是否 >0。是的!
线程 2 acc.getValue >0。是的!
线程1调用递减。值为 0
线程 2 调用递减。现在的值为 -1
发生什么事了?每个功能都确保它不会低于零,但他们一起设法做到了这一点。这称为竞争条件。
为避免这种情况,您不能保护基本操作,而是保护任何必须不间断执行的代码片段。
因此,这个 class 是线程安全的,但用途非常有限。
代码不是线程安全的。
假设一个线程调用 decrement
,然后第二个线程调用 getValue
。会发生什么?
问题是 decrement
和 getValue
之间没有 "happens before" 关系。这意味着无法保证 getValue
调用会看到 decrement
的结果。事实上,getValue
可以 "miss" 无限序列的 increment
和 decrement
调用的结果。
实际上,除非我们看到使用 Account
class 的代码,否则线程安全问题是不明确的。程序的线程安全1 的传统概念是关于代码是否正确 行为,而与线程相关的不确定性无关。在这种情况下,我们没有关于什么是 "correct" 行为的规范,或者实际上没有要测试或检查的可执行程序。
但是我对代码2的阅读是有一个暗示API要求/正确性标准getValue
returns 账户的 当前 值。如果有多个线程则无法保证,因此 class 不是线程安全的。
相关链接:
1 - @CKing 的回答中的并发实践引用也通过在定义中提及 "invalid state" 来吸引 "correctness" 的概念。但是,关于内存模型的 JLS 部分没有指定线程安全。相反,他们谈论 "well-formed executions".
2 - OP 在下面的评论支持此阅读。但是,如果您不接受这个要求是真实的(例如因为它没有明确说明),那么另一方面是 "account" 抽象的行为取决于 [=19= 之外的代码如何] class ...这使它成为 "leaky abstraction".
简答:
根据定义,Account
是 线程安全的 class 即使 geValue
方法不受 保护
长答案
来自Java 实践中的并发性 class 在以下情况下被认为是线程安全的:
No set of operations performed sequentially or concurrently on instances of a thread-safe class can cause an instance to be in an invalid state.
由于 getValue
方法不会导致 Account
class 在任何给定时间处于无效状态,因此您的 class 被认为是线程安全的。
Collections#synchronizedCollection 的文档引起了这种情绪的共鸣:
Returns a synchronized (thread-safe) collection backed by the specified collection. In order to guarantee serial access, it is critical that all access to the backing collection is accomplished through the returned collection. It is imperative that the user manually synchronize on the returned collection when iterating over it:
Collection c = Collections.synchronizedCollection(myCollection); ... synchronized (c) { Iterator i = c.iterator(); // Must be in the synchronized block while (i.hasNext()) foo(i.next()); }
注意文档是如何说集合(它是 Collections
class 中名为 SynchronizedCollection
的内部 class 的对象)是 thread-safe 并要求客户端代码在遍历集合时保护集合。事实上,SynchronizedCollection
中的iterator
方法不是synchronized
。这与您的示例非常相似,其中 Account
是线程安全的,但客户端代码在调用 getValue
.
这不是线程安全的,纯粹是因为无法保证编译器如何重新排序。由于值不是易变的,这里是您的经典示例:
while(account.getValue() != 0){
}
这可以吊起来看起来像
while(true){
if(account.getValue() != 0){
} else {
break;
}
}
我可以想象还有其他编译器乐趣的排列可能会导致它巧妙地失败。但是通过多线程访问此 getValue
可能会导致失败。
这里有几个不同的问题:
问:如果多个线程重叠调用 increment()
和 decrement()
,然后它们停止,然后 足够的时间 没有线程调用increment()
或 decrement()
,getValue() return 会是正确的数字吗?
答:是的。递增和递减方法中的锁定确保每个递增和递减操作将自动发生。他们不能互相干扰。
问:多长时间足够?
A:这很难说。 Java 语言规范不保证调用 getValue() 的线程将 永远 看到其他线程写入的最新值,因为 getValue() 访问该值时没有任何同步全部.
如果您更改 getValue() 以锁定和解锁同一个锁定对象,或者如果您将计数声明为 volatile
,那么零时间就足够了。
问:可以调用 getValue()
return 一个 无效的 值吗?
A: 不,它只能 return 初始值,或完整 increment()
调用的结果或完整 decrement()
操作的结果。
但是,这样做的原因与锁无关。该锁 不会 阻止任何线程调用 getValue()
而其他线程正在递增或递减值。
阻止 getValue() return 获取完全无效值的原因是该值是一个 int
,并且 JLS 保证 int
变量的更新和读取是总是原子的。