java - 如何在没有内存可见性影响的情况下使一组语句原子化?

标签 java c++ multithreading concurrency atomic

synchronized块让我使一组语句原子化,同时确保块退出和进入之间存在发生之前的关系。

I read that the biggest cost of synchronization are the memory visibility guarantees, not the lock contention.
假设我能够通过其他方式保证内存可见性:

如何使一组语句原子化,而不创建发生在之前的关系,即没有 synchronized 的内存可见性影响/Lock ?

我尝试通过 CAS 在用户空间中实现锁定,但内置的性能远远超过它,并且 CAS 变量仍然发出内存障碍。

在这个例子中,没有内存可见性效果的互斥锁就足够了。

(release/acquire) int x; // Variable with release/acquire semantics

// release fence
synchronized (this) {

    int y = x;
    // acquire fence

    // release fence
    x = 5;

}
// acquire fence

同一组栅栏被发射两次(通过互斥锁和 x)。
它会导致不必要的开销吗?

理论上没有内存效应的锁是可能的吗?
没有内存效应的锁实际上会更高效吗?
是否有内置的方法可以在 C++ 和/或 Java 中完成此操作?
如果没有,它可以用 C++ 和/或 Java 实现吗?

最佳答案

在互斥锁中保证内存可见性的成本可以忽略不计,实际上在 x86 上它是免费的。

获取互斥锁需要具有获取语义的原子读-修改-写操作。对于释放互斥锁,使用具有释放语义的简单存储就足够了。考虑一个简单的自旋锁 - 获取操作由一个循环组成,该循环反复尝试将锁标志设置为 1(如果当前为 0)。要释放锁,拥有线程只需将 0 写入锁标志。在许多方面,这样一个简单的自旋锁远非最佳,并且有许多锁的设计试图改进它(例如,公平性,在本地缓存行上旋转等),但在所有这些设计中释放锁是当然比购买它便宜。

x86 内存模型非常强大:所有原子读-修改-写操作都是顺序一致的,所有存储操作都有有效的释放-,所有加载操作都获得语义。这就是为什么在 x86 上释放互斥锁可以通过普通存储完成,不需要额外的指令来确保内存效果的可见性。在具有较弱内存模型的架构(如 ARM 或 Power)上,您确实需要额外的指令,但与获取操作的成本相比,成本可以忽略不计。 x86 也有特殊的屏障指令,但那些通常只在无锁编程中的某些情况下相关,并且这些指令的成本与一些原子读修改写大致相同。

互斥锁的真正成本不是内存效应的可见性,而是争用和执行的序列化。如果竞争互斥锁的线程数量少,并且线程持有互斥锁的持续时间也很低,那么对整体性能的影响也会很小。但是如果争夺互斥锁的线程数量多,并且一个线程持有互斥锁的持续时间也很大,那么其他线程将不得不等待更长的时间,直到他们最终获得互斥锁并继续执行。这减少了在给定时间范围内可以执行的工作。

我不确定您所说的“理论上是否可以实现没有内存效应的锁?”是什么意思。互斥锁的全部目的是允许一些操作被执行 - 并且也被观察 - 就好像它们是原子的一样。这意味着该操作的效果对互斥锁的下一个所有者可见。这实际上是happens-before 关系所保证的。如果线程 A 获取互斥锁,并且此获取操作发生在某个线程 B 的释放操作之后,那么由于发生在关系的传递性,B 在持有互斥锁时执行的操作一定在操作之前发生A 即将执行 - 这意味着所有内存效应都必须可见。如果这不能保证,那么您的互斥锁就会损坏并且您会遇到竞争条件。

关于示例中的 volatile 变量 - Java 内存模型要求对共享 volatile 变量的所有操作顺序一致。但是,如果 x 仅在临界区中被访问(即,受某个互斥锁保护),则它不必是 volatile。仅当某些线程在没有任何其他同步机制(如互斥锁)的情况下访问变量时才需要 Volatile。

互斥体操作的释放/获取语义是对互斥体内部的操作进行排序所必需的。在 C++ 中,可以使用宽松的操作实现互斥锁。互斥锁本身的锁定/解锁操作仍然是完全有序的(由于互斥锁的修改顺序),但我们将失去发生之前的关系,因此互斥锁内的操作将是无序的。虽然这在 C++ 中是可能的,但这将是相当荒谬的,因为正如我试图解释的那样,使内存效应可见是非常便宜的(在 x86 上它是免费的),但是你会失去一个在几乎所有情况下都绝对至关重要的属性.注意:释放互斥锁的存储操作比存储到 volatile 变量便宜。可变变量是顺序一致的,但释放互斥锁可以通过释放存储来完成。 (当然,Java 内存模型不如 C++ 模型灵活,因此您无法使用更轻松的获取/释放操作来真正实现手工编织的锁)。

关于java - 如何在没有内存可见性影响的情况下使一组语句原子化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61662731/

相关文章:

c++ - 使用 unordered_map 将对象映射为键

Java:通过多线程并行化快速排序

java - 多个移动球 : Adding Ball works but removing does not

java - java中的正则表达式错误

java - 运行简单的 Camel ReSTLet 演示项目时遇到问题?

c++ - 如何在 openGL 中正确显示球体

java - 在 Jsplitpane 中同时显示两个面板且同一面

C++ 成员函数指针用例?

java - 在 Java 中处理 HTTP 调用的大文件

java - Tomcat 中的线程池和请求处理