class A {
private static BigInteger staticCode = BigInteger.ZERO;
private BigInteger code;
public A() {
synchronized(staticCode) {
staticCode = staticCode.plus(BigInteger.ONE);
code = staticCode;
}
}
}
无论如何,我都不是并发方面的专家。有人能向我解释一下为什么上面提供的类不是线程安全的吗?
哪些情况会导致比赛状况?我的想法是,如果我们创建这个类的10个实例,每个实例都将在不同的staticCode
值上同步,这就是为什么它是线程安全的,但我被告知它不是。但为什么呢?
我知道我们可以在.class
上同步,它肯定是线程安全的,但我仍然想了解这种特殊情况。
在正在修改的静态字段上进行同步是否可以确保代码线程的安全?
不,因为您正在重新分配它。(*(
重新分配后,您实际上就失去了访问staticCode
字段的互斥性。
- 在分配之前已经在
synchronized
块等待的任何线程都将继续等待 -
在重新分配之后但在重新分配线程离开块之前到达
synchronized
块的任何线程将尝试在staticCode
的新值上同步。比没有互斥的事实更微妙的一点是,在同步块结束和下一次执行开始之间,还会丢失之前发生的事情。这意味着您不能保证更新值的可见性,因此您可能会生成具有相同
code
的多个A
实例。
在非最终成员上同步是个坏主意。如果你不想在A.class
上同步,你可以定义一个要同步的辅助成员:
class A {
private static final Object lock = new Object();
private static BigInteger staticCode = BigInteger.ZERO;
public A() {
synchronized (lock) {
staticCode = ...
}
}
}
这保留了staticCode
的可变性,但允许正确的互斥。
然而,Atomic*
类会容易得多,因为您可以避免同步的需要(例如AtomicInteger
或AtomicLong
,但如果您真的认为您将拥有超过2^63的东西,则可以使用AtomicReference<BigInteger>
(:
class A {
private static final Object lock = new Object();
private static AtomicReference<BigInteger> staticCode = new AtomicReference<>(BigInteger.ZERO);
public A() {
BigInteger code;
do {
code = staticCode.get();
} while (!staticCode.compareAndSet(code, code.add(BigInteger.ONE)));
this.code = code;
// Even easier with AtomicInteger/Long:
// this.code = BigInteger.valueOf(staticCode.incrementAndGet());
}
}
(*(但无论如何,不要再考虑同步会自动确保线程安全的概念。首先,您需要准确定义"线程安全"的含义;但是,您需要了解同步实际上为您提供了什么,以便评估这些东西是否满足您的线程安全要求。
我想我在这里缺少的要点是我们在对象上同步,而不是对对象的引用。
考虑一种情况,我在BigInteger.ZERO上同步,然后输入同步块。
当staticCode的值发生变化并变为BigInteger.ONE时,该块仍将继续在BigInteger.ZERO上同步。同时,在我们将BigInteger.NE分配给代码之前,另一个线程已经在BigInteger.ONE上同步。第二个线程可以将staticCode的值提升到2,现在两个线程都在第二次赋值之前,但staticCode的数值是2,因此它们都可以将相同的staticCode值赋值给类的两个不同实例。