我正在准备OCP考试,我在模拟考试中发现了这个问题:
假设:
class Calculator {
private AtomicInteger i = new AtomicInteger();
public void add(int value) {
int oldValue = i.get();
int newValue = oldValue + value;
System.out.print(i.compareAndSet(oldValue,newValue));
}
public int getValue() {
return i.get();
}
}
你能做些什么来使这个类线程安全?
令人惊讶的是,对我来说,答案是: "类计算器是线程安全的"
一定是我没有正确理解这个概念。据我所知,当所有方法在线程并发下按预期工作时,类是线程安全的。现在,如果两个线程同时调用getValue(),则调用add()传递一个不同的值,然后再次调用getValue(),' second'线程不会看到它的价值增加了。
我理解oldValue和newValue作为局部变量存储在方法堆栈中,但这并不能防止对compareAndSet的第二次调用会发现oldValue不是当前值,因此赢得了#39 ; t添加newValue。
我在这里缺少什么?
答案 0 :(得分:3)
根据JCIP
如果一个类在从多个线程访问时行为正确,则无论调度如何,该类都是线程安全的 由运行时环境交错执行这些线程,没有额外的同步或 调用代码的其他协调。
虽然没有线程安全的定义,也没有类的规范,但在我看来,add
类中Calculator
方法的任何理智定义都是“正确的”在任何情况下,AtomicInteger i
的值都会增加,“无论执行的调度或交错如何”。
因此,在我看来,这个定义的类不是线程安全的。
答案 1 :(得分:3)
这里的术语“线程安全”显然存在问题,因为它不是绝对的。什么被认为是线程安全取决于您期望程序做什么。在大多数实际应用程序中,您不会认为此代码是线程安全的。
然而,JLS正式指定了一个不同的概念:
程序正确同步当且仅当所有顺序执行时 一致的执行没有数据竞争。
如果一个程序正确同步,那么所有的执行都是 程序似乎是顺序一致的
正确同步是一个精确定义的客观条件,根据该定义,代码正确同步,因为every access to i
is in a happens-before
relationship with every other access,这足以满足标准。
读/写的确切顺序取决于不可预测的时序这一事实是一个不同的问题,在正确同步的范围之外(但在大多数人称之为线程安全的范围内)。
答案 2 :(得分:2)
add
方法将执行以下两项操作之一:
value
添加到i
的值,然后打印true
。false
。我所看到的线程安全的理论上声音 1 的定义是这样的:
给定一组要求,如果程序在多线程环境中针对所有可能执行的要求更正,那么程序就这些要求而言是的 。
在这种情况下,我们没有明确的要求声明,但如果我们推断预期的行为如上所述,那么该类就线程安全而言相对于推断要求。
现在,如果要求add
总是将value
添加到i
,则该实施不符合要求。在这种情况下,您可以认为该方法不是线程安全的。 (在单线程用例add
中始终有效,但在多线程用例中,add
方法偶尔可能无法满足要求...因此将是非线程安全的。)
1 - 相比之下,维基百科的描述(见于2016-01-17)是这样的:"如果一段代码只操纵一个代码中的共享数据结构,它就是线程安全的。保证多个线程同时安全执行的方式。" 问题在于它没有说出什么"安全执行"手段。 Eric Lippert的2009年博文"What is this thing you call thread-safe"是非常恰当的。
答案 3 :(得分:0)
它是线程安全的,因为compareAndSet是线程安全的,而且它是修改对象中共享资源的唯一部分。
有多少线程同时进入该方法体并没有什么区别。第一个到达结尾并调用compareAndSet“wins”并获取更改值,而其他人发现值已更改,并且compareAndSet返回false。它永远不会导致系统处于不确定状态,尽管调用者可能不得不在任何实际场景中处理错误结果。