2

在 Java 中,每个对象都有一个同步监视器。所以我猜这个实现在内存使用方面非常简洁,希望也很快。

将其移植到 C++ 时,最好的实现是什么。我认为一定有比“pthread_mutex_init”更好的东西,还是java中的对象开销真的这么高?

编辑:我刚刚检查了 Linux i386 上的 pthread_mutex_t 是 24 字节大。如果我必须为每个对象保留这个空间,那将是巨大的。

4

4 回答 4

3

从某种意义上说,它pthread_mutex_init实际上比 更糟糕。由于 Java 的等待/通知,您需要一个配对的互斥锁和条件变量来实现监视器。

在实践中,在实现 JVM 时,您会寻找并应用书中每一个特定于平台的优化,然后发明一些新的优化,以尽可能快地制作监视器。如果你不能做一个非常糟糕的工作,那么你肯定不会优化垃圾收集;-)

一项观察是,并非每个对象都需要有自己的监视器。当前未同步的对象不需要同步。因此 JVM 可以创建一个监视器池,每个对象可以只有一个指针字段,当线程实际想要在对象上同步时填充该字段(例如,使用特定于平台的原子比较和交换操作)。因此监视器初始化的成本不必增加对象创建的成本。假设内存被预先清除,对象创建可以是:递减一个指针(加上某种边界检查,预测错误分支到运行 gc 的代码等等);填写类型;调用最派生的构造函数。我认为您可以安排 Object 的构造函数什么都不做,但显然很大程度上取决于实现。

在实践中,一般的 Java 应用程序在任何时候都不会同步很多对象,因此监视器池可能是时间和内存方面的巨大优化。

于 2009-11-18T00:57:21.390 回答
2

Sun Hotspot JVM使用compare 和 swap实现。如果对象被锁定,则等待线程等待锁定对象的线程的监视器。这意味着每个线程只需要一个重锁。

于 2009-11-18T00:58:59.797 回答
2

我不确定 Java 是如何做到的,但 .NET 不会直接在对象中保留互斥锁(或模拟 - 保存它的结构称为“syncblk”)。相反,它有一个全局同步块表,对象通过该表中的索引引用它的同步块。此外,对象不会在创建后立即获得同步块 - 相反,它是在第一次锁定时按需创建的。

我假设(注意,我不知道它实际上是如何做到的!)它使用原子比较和交换以线程安全的方式关联对象及其同步块:

  1. 检查我们对象的隐藏syncblk_index字段是否为 0。如果不是 0,则将其锁定并继续,否则...
  2. 在全局表中创建一个新的syncblk,获取它的索引(全局锁在此处根据需要获取/释放)。
  3. 比较并交换将其写入对象本身。
  4. 如果之前的值为 0(假设 0 不是有效索引,并且是syncblk_index我们对象的隐藏字段的初始值),我们的 syncblk 创建没有争议。锁定它并继续。
  5. 如果先前的值不是 0,那么在我们创建我们的对象时,其他人已经创建了一个 syncblk 并将其与对象相关联,我们现在有了该 syncblk 的索引。处理我们刚刚创建的那个,并锁定我们已经获得的那个。

因此,在最佳情况下,每个对象的开销为 4 字节(假设 syncblk 表中的 32 位索引),但对于实际已锁定的对象来说更大。如果您很少锁定您的对象,那么此方案看起来是减少资源使用的好方法。但是,如果您最终需要锁定大部分或所有对象,则直接在对象中存储互斥锁可能会更快。

于 2009-11-18T01:06:12.573 回答
1

当然,您不需要为每个对象配备这样的监视器!

当从 Java 移植到 C++ 时,我觉得盲目地复制所有内容是个坏主意。Java 的最佳结构与 C++ 的最佳结构不同,尤其是因为 Java 具有垃圾收集功能而 C++ 没有。

仅将监视器添加到那些真正需要它的对象。如果只有某个类型的某些实例需要同步,那么创建一个包含同步所需的互斥锁(可能还有条件变量)的包装类并不难。正如其他人已经说过的那样,另一种方法是使用同步对象池,并通过某种方式为每个对象选择一个对象,例如使用对象地址的哈希来索引数组。

我会使用 boost 线程库或新的 C++0x 标准线程库来实现可移植性,而不是每次都依赖于平台细节。Boost.Thread支持 Linux、MacOSX、win32、Solaris、HP-UX 等。我的 C++0x 线程库实现目前仅支持 Windows 和 Linux,但其他实现将在适当时候推出。

于 2009-11-18T11:04:21.003 回答