sun.misc.Unsafe.putOrdered 应该做你想做的事 - 一个存储,其锁在 x86 上由 易失性隐含。我相信编译器不会围绕它移动指令。
这与 AtomicInteger 等上的lazySet 相同,但不能直接与 ByteBuffer 一起使用。
Unlike volatile
or the AtomicThings
类,该方法适用于您使用它的特定写入,而不是成员的定义,因此使用它并不意味着任何读取。
看起来你正在尝试实现类似的东西seqlock http://en.wikipedia.org/wiki/Seqlock- 这意味着您需要避免在版本计数器的读取之间重新排序,A
,以及数据本身的读/写。普通的 int 并不能解决问题——因为 JIT 可能会做各种顽皮的事情。我的建议是为您的计数器使用 volatile int ,然后将其写入putOrdered
。这样,您就不必为易失性写入(通常是十几个周期或更多)付出代价,同时获得易失性读取所隐含的编译器屏障(并且这些读取的硬件屏障是无操作的,从而使它们更快) )。
话虽如此,我认为你处于灰色地带,因为lazySet
不是正式内存模型的一部分,并且不能完全符合发生前推理,因此您需要更深入地了解实际的 JIT 和硬件实现,看看是否可以通过这种方式组合事物。
最后,即使有易失性的读取和写入(忽略lazySet
),我认为从java内存模型的角度来看你的seqlock并不合理,因为易失性写入仅在另一个线程上的写入和稍后的读取之间以及写入线程中的早期操作之间建立一个发生之前,但是不在写入线程上的读取和写入操作之间。换句话说,它是单向围栏,而不是双向围栏。我相信,即使读取 A == N 两次,读取线程也可以看到版本 N+1 中对共享区域的写入。
评论中的澄清:
挥发性只建立了一个单向障碍。它与 WinTel 在某些 API 中使用的获取/释放语义非常相似。例如,假设 A、Bv 和 C 最初全部为零:
Thread 1:
A = 1; // 1
Bv = 1; // 2
C = 1; // 3
Thread 2:
int c = C; // 4
int b = Bv; // 5
int a = A; // 6
这里,只有 Bv 是不稳定的。这两个线程正在执行与 seqlock 写入器和读取器概念类似的操作 - 线程 1 按一个顺序写入一些内容,线程 2 以相反的顺序读取相同的内容,并尝试从中推理出顺序。
如果线程二有 b == 1,则 a == 1 总是,因为 1 发生在 2 之前(程序顺序),而 5 发生在 6 之前(程序顺序),最关键的是 2 发生在 5 之前,因为 5 读取写入的值at 2. 因此,通过这种方式,Bv 的写入和读取就像栅栏一样。上面 (2) 的东西不能“移动到下面”(2),并且下面的东西 (5) 不能“移动到上面”5。请注意,我只限制每个线程直接在一个线程中移动,但不能同时限制两者的移动,这将我们带到下一个例子:
与上述相同,您可能会假设如果 a == 0,则 c == 0 也一样,因为 C 写在 a 之后,读取在 a 之前。然而,挥发物并不能保证这一点。特别是,上面发生的推理不会阻止 (3) 被移动到 (2) 之上(如线程 2 所观察到的),也不会阻止 (4) 被推到 (5) 之下。
Update:
让我们具体看看你的例子。
我相信可能发生的是这样的,展开 p1 中发生的写循环。
p1:
i = 0
A = 0
// (p1-1) write data1 to B
A = ++i; // (p1-2) 1 assigned to A
A=0 // (p1-3)
// (p1-4) write data2 to B
A = ++i; // (p1-5) 2 assigned to A
p2:
a1 = A // (p2-1)
//Read from B // (p2-2)
a2 = A // (p2-3)
if a1 == a2 and a1 != 0:
假设 p2 看到 a1 和 a2 均为 1。这意味着 p2-1 和 p1-2(以及扩展 p1-1)之间以及 p2-3 和 p1-2 之间都有一个发生。然而,p2 和 p1-4 中的任何内容之间都有发生之前的情况。所以事实上,我相信在 p2-2 处读取 B 可以观察到在 p1-4 处的第二次(可能部分完成)读取,它可以“移动到”p1-2 和 p1-3 处的易失性写入之上。
这很有趣,我认为你可能会仅仅就这一点提出一个新问题 - 忘记更快的障碍 - 即使对于 挥发性 ,这是否也有效?