易变的背负式。这足以提高知名度吗?
Posted
技术标签:
【中文标题】易变的背负式。这足以提高知名度吗?【英文标题】:Volatile piggyback. Is this enough for visiblity? 【发布时间】:2012-02-04 21:00:15 【问题描述】:这是关于不稳定的搭载。 目的:我想达到一个轻量级的变量可见性。 a_b_c 的一致性并不重要。我有一堆 var,我不想让它们都不稳定。
这段代码是线程安全的吗?
class A
public int a, b, c;
volatile int sync;
public void setup()
a = 2;
b = 3;
c = 4;
public void sync()
sync++;
final static A aaa = new A();
Thread0:
aaa.setup();
end
Thread1:
for(;;) aaa.sync(); logic with aaa.a, aaa.b, aaa.c
Thread2:
for(;;) aaa.sync(); logic with aaa.a, aaa.b, aaa.c
【问题讨论】:
如果一致性不重要,你为什么要关心变量是否已经同步? 我不关心一致性,但我关心可见性。 然后将它们公开?但是为什么要用同步来标记这个问题呢? 我将它们公开)。易失性、缓存行、内存屏障这些都是关于同步的。 【参考方案1】:您根本不需要手动同步,只需使用自动同步的数据结构,例如java.util.concurrent.atomic.AtomicInteger
。
您也可以将sync()
方法设为synchronized
。
【讨论】:
我根本不关心同步变量【参考方案2】:仅使用volatile
在线程之间增加一个值绝不是线程安全的。这只能确保每个线程都获得最新的值,而不是增量是原子的,因为在汇编程序级别,您的 ++ 实际上是可以交错的几条指令。
您应该使用AtomicInteger
进行快速原子增量。
编辑:再读一遍你需要的其实是一个记忆栅栏。 Java 没有内存栅栏指令,但您可以为内存栅栏“副作用”使用锁。在这种情况下,声明同步方法 synchronized 以引入隐式围栏:
void synchronized sync()
sync++;
【讨论】:
@temper:我意识到了这一点。编辑是您问题的答案。【参考方案3】:来自 javadoc:
监视器的解锁(同步块或方法退出) 发生在每个后续锁之前(同步块或方法 同一个监视器的条目)。因为happens-before关系 是传递的,线程在解锁之前的所有动作 发生在任何线程锁定之后的所有操作之前 监视器。
对 volatile 字段的写入发生在每次后续读取之前 同一个领域。 volatile 字段的写入和读取具有相似的 内存一致性效果作为进入和退出监视器,但确实 不需要互斥锁定。
所以我认为在这种情况下写入 volatile var 并不等同于同步,它不能保证 Thread1 到 Thread2 中的更改的先发生顺序和可见性
【讨论】:
【参考方案4】:Java 内存模型定义了 happens-before 关系,它具有以下属性(以及其他属性):
“线程中的每个动作都发生在该线程中按程序顺序后面的每个动作之前”(程序顺序规则) “对 volatile 字段的写入发生在每次后续读取同一 volatile 之前”(volatile 变量规则)这两个属性连同 happens-before 关系的传递性暗示了 OP 以下列方式寻求的可见性保证:
-
在线程 1 中写入
a
发生之前在线程 1 中调用 sync()
时写入 sync
(程序顺序规则)。
在线程 1 中对 sync()
的调用中写入 sync
之前发生在线程 2 中对 sync
的调用中对 sync
的读取(易失性变量规则) .
在线程 2 中对 sync()
的调用中从 sync
读取之前发生在线程 2 中从 a
读取(程序顺序规则)。
这意味着问题的答案是肯定的,即在线程 1 和 2 的每次迭代中调用 sync()
可确保对 a
、b
和 c
的更改对另一个线程的可见性( s)。请注意,这仅确保可见性。不存在互斥保证,因此可能会违反绑定a
、b
和c
的所有不变量。
另见Java theory and practice: Fixing the Java Memory Model, Part 2。特别是“波动的新保证”部分,它说
在新的内存模型下,当线程 A 写入一个 volatile 变量 V,线程 B 从 V 中读取任何变量值 在写入 V 时对 A 可见,现在保证为 B 可见。
【讨论】:
@PhilippWendler 这两个规则意味着对a
、b
和c
的更改可见性在线程1 到线程2 中以下列方式进行:在线程1 中更改为a
happens-before 在线程 1(规则 1)中对 sync()
的后续调用中写入 sync
,其中 happens-before 在调用中从 sync
读取到线程 2 中的 sync
(规则 2)发生之前从线程 2 中的 a
读取(同样,规则 1)。
@PhilippWendler 另请参阅我链接到的文章,特别是“volatile 的新保证”部分。另一个相关的引用:“在新的内存模型下,当线程 A 写入易失性变量 V,而线程 B 从 V 读取时,在写入 V 时对 A 可见的任何变量值现在都保证是可见的给 B."
你能解释一下change to a in thread 1 happens-before a write to sync in a subsequent call to sync() in thread 1 (rule 1)
。我看到的是aaa.sync(); logic with aaa.a, aaa.b, aaa.c
,所以sync()
在访问a
之前被调用。谢谢
“对sync()
的后续调用”将在下一次围绕包含其余逻辑的无限循环进行。
@Adam Zalcman 请注意,这仅确保可见性。不存在互斥保证,因此所有绑定 a、b 和 c 的不变量都可能被违反。 请您澄清这句话。【参考方案5】:
模式通常是这样的。
public void setup()
a = 2;
b = 3;
c = 4;
sync();
但是,虽然这保证了其他线程会看到此更改,但其他线程可以看到不完整的更改。例如Thread2
可能会看到 a = 2、b = 3、c = 0。甚至可能看到 a = 2、b = 0、c = 4;
在阅读器上使用 sync() 并没有多大帮助。
【讨论】:
我有误会。在 thread1 我调用 setup 方法。在 thread2 之后我可以看到 b=0 吗? @g*** 你需要两个部分。在写入端的末尾有一个写屏障,在读取端的开头有一个读屏障。这可以确保您阅读在另一个线程中写入的内容。写入 volatile 涉及写入屏障,读取 volatile 涉及读取屏障。 @PeterLawrey,但在这个问题中,他确实有阅读障碍,对吧?线程 1 和线程 2 中的aaa.sync()
是否足够?
sync() 确实是读写障碍(因为它读取、递增然后写入),因此在阅读器之前调用 sync
应该会有所帮助。是的,同步的增量不会是原子的,但是这个变量的值不包含任何逻辑,除了捎带以上是关于易变的背负式。这足以提高知名度吗?的主要内容,如果未能解决你的问题,请参考以下文章