Java是否保证当前同步对象不被垃圾回收?

6

一个线程持有对象的监视器时,是否有任何保证该对象不会被垃圾回收?

例如:

class x {
    private WeakReference<Object> r;

    Object getMonitorObject() {
        Object o = new Object();
        r = new WeakReference<>(o);
        return o;
    }

    void thread1() throws Exception {
        synchronized (getMonitorObject()) {
            Thread.sleep(3000);
        }
    }

    void thread2() {
        Object b = r.get();
    }
}

在这种情况下,如果在调用thread2()时另一个线程正在thread1()中睡眠,是否有保证b不会为空? 假设在不同的线程中睡眠thread1()期间执行了整个thread2()

虽然JLS没有严格规定synchronized块的目标必须被视为“强引用”,但这一暗示是清晰的,实际上也是如此。这段代码中唯一的问题在于WeakReference r需要是volatile(或使用其他内存屏障)以确保分配的值对第二个线程可见。 - erickson
@erickson 是的,r 应该是易失性的。 - Jesse
@erickson 我认为这个暗示并不清晰。请参考Holger提供的JLS 12.6.1链接。他们考虑了同步是否会保持对象的存活状态,并在特定情况下制定了规则,但并非总是如此。 - Jesse
是的,请忽略我评论的前半部分。我正在搜索JLS 14和17以及java.lang.ref包文档,但我没有考虑到终结器的后果(现在不鼓励使用它)。 - erickson
5个回答

3

同步可以防止垃圾回收,但并不是普遍适用的。在您特定的情况下,不能保证它。

参考 JLS §12.6.1

...

这种转换可能会导致 finalize 方法的调用早于预期。为了允许用户防止这种情况发生,我们强制执行同步可能会使对象存活的概念。如果一个对象的 finalizer 可能会对该对象进行同步,则每当锁定该对象时,该对象必须处于存活状态且可到达。

请注意,这并不能防止同步消除:同步只有在 finalizer 可能对其进行同步时才会使对象存活。由于 finalizer 是在另一个线程中执行的,在许多情况下,同步无法被删除。

因此,由于您的对象没有自定义的 finalizer,在 finalization 期间没有同步发生的情况下,原则上您的对象是一个允许锁消除的临时对象,这种情况下它不会防止垃圾回收。

但是有一个实际的障碍,即您以另一线程可能在对象未被收集时检索该对象的方式存储了一个WeakReference,一旦存在这种可能性,该对象就不再是本地的,因此无法应用锁消除。

理论上的实现可以在构建后立即积极收集对象(或完全消除其存在),并在其逃逸之前清除弱引用或首先创建一个空的WeakReference,这将在规范内,因为在该执行场景中,锁消除是合理的。


请注意,即使您插入了reachabilityFence,在调用thread1()的线程和另一个调用thread2()的线程之间没有happens-before关系,因此第二个线程始终可能表现为在另一个完成synchronized块或通过可达性栅栏之后执行thread2(),即使您的真实生活时钟告诉您不同。明确指出Thread.sleep没有同步语义。

在JLS 12.6.1中提到:“仅当finalizer可能对其进行同步时,同步才会使对象保持活动状态”。这基本上告诉我,如果没有可能在对象上同步的finalizer,则同步本身不能保证使对象保持活动状态。此外,是的,r应该是易失性的。 Thread.sleep只是为了展示thread1()需要很长时间。其他回答者的测试和逻辑表明,在实践中,同步确实使对象保持活动状态,但我特别要求保证,并且我认为这是正确的答案-没有保证。 - Jesse
1
@Jesse,该句子包含在引用文本中。此外,请注意,即使将r声明为volatile,它仅保证thread2()的调用者能够正确地看到弱引用(如果已经写入),但仍不能保证thread1仍然在synchronized块内部,因为离开synchronized块和任何事情之间仍存在排序关系,而thread2则不受此限制。 - Holger
那很有道理。谢谢。 - Jesse

2
为了让同步块的结尾能够释放监视器锁,同步块必须保留对由getMonitorObject()返回的对象的引用。这个引用会防止垃圾回收,所以答案是肯定的。

这听起来很实际,但有没有任何保证?似乎有道理的是,线程只需要保留对 监视器 本身的引用,而不是它所拥有的对象。对象和监视器之间可能存在差异。 - Jesse
2
@Jesse 在Java中,对象就是监视器。 - Andreas
2
JLS 17.1:“Java中的每个对象都与监视器相关联,线程可以锁定或解锁该监视器”。稍后:“同步语句计算对象的引用;然后尝试对该对象的监视器执行锁定操作”。听起来可能存在对象和监视器之间的差异。 - Jesse
3
实际上它没有什么区别。在我遇到的所有Java实现中,监视器/锁状态的一部分都在对象的头部。 - Stephen C

2
在我详细研究过的所有Java实现中,对象的原始互斥锁状态部分地由对象头中的位表示。当锁被释放时,JVM需要更新头部位,因此它必须仍然引用对象,无论是在堆栈上还是寄存器中。
当发生互斥锁争用时,锁会“膨胀”以记录额外信息。因此,我们不能说整个状态都在对象头中。
在大多数情况下,解锁代码不知道锁对象是否不可达。但如果由于激进的JIT编译器优化而确实这样做了,那么假设它也可以知道更新对象头已不再必要。
假设在解锁互斥量时未使用/需要对象引用。
以下是JLS 12.6.1中可达性的定义:
“可达对象是指可以从任何活动线程中的任何潜在持续计算访问的任何对象。” “最终器可达对象可以通过某些引用链从某些可终结对象到达,但不能从任何活动线程到达。” “不可达对象无法通过任何方式到达。”
为了使对象成为垃圾回收的候选对象,它必须不可达。换句话说,它不能从任何活动线程访问。
那么锁定的互斥量呢?好吧,线程的“潜在持续计算”可能需要解锁互斥量:
  • 如果互斥锁无法在没有对象的引用的情况下解锁,则该对象是可达的。
  • 如果互斥锁可以在没有对象引用的情况下解锁,则该锁可能是不可达的,或者是由终结器引用的。

但是等一下...
JLS 12.6.2 中,有一些关于可达性决策点的复杂语言。
每到一个可达性决策点,一些对象被标记为不可达,其中一些对象被标记为可终止。
如果对象X在di处被标记为不可达,则: - ... - 线程t中X的所有活动使用,在di之后发生必须出现在X的finalizer调用中或者由线程t执行读操作以使得对X的引用出现在di之后,并且... - ...
如果动作a是对X的活动使用,则当以下至少一种情况成立时,动作a是对X的活动使用: - ... - a锁定或解锁X并且存在X上的锁定动作发生在X的finalizer调用之后。 - ...
现在如果我理解正确,这意味着不能对可终止的对象进行终结,如果仍然有活动线程可能释放相应的互斥量。
总结如下:
  1. 在当前的JVM中,互斥锁的锁状态取决于对象头。如果一个互斥锁仍然可以被线程释放,那么对象必须作为实现细节可达。

  2. 假设存在一种JVM,可以在不引用对象的情况下释放互斥锁,则对象可能在被锁定时不可访问。

  3. 但是,如果对象是可终止的,则在所有可能需要释放锁的活动(应用程序)线程完成之后才会对其进行终止。


谢谢你的分析,我非常感激。我同意,在实践中,对象不能在不销毁监视器的情况下被销毁。我不同意你关于JLS 12.6.2的结论:简单地解锁不是对X主动使用。如果a解锁X并且在调用X的终结器之后发生了X上的锁定操作,则a主动使用。我的场景不包括“and”之后的部分。然而,我认为你说得对,在实践中,同步确实会使对象保持活动状态,但我不认为JLS在这种情况下有保证。 - Jesse

1
我刚在javadocs中看到了一个“aside”,其内容如下:
“[该]方法reachabilityFence在本身确保可达性的构造中并不需要。例如,由于锁定的对象通常无法被回收,因此如果类Resource的所有方法(包括finalize)中的所有访问都包含在synchronized(this)块中,则足以满足要求。”
这似乎意味着被锁定的对象无法进行垃圾回收。
我不确定这是否优先于JLS 12.6.1和12.6.2;请参阅我的其他答案,或者我们应该将其解读为仅适用于Oracle / OpenJDK Java类库的Java(语言)实现。

我认为这涉及到实际问题。正如你所解释的那样,对象不能被销毁而不破坏互斥锁。我认为这并不是在说“禁止销毁已锁定的对象”,更像是“销毁已锁定的对象是不切实际的”。再次感谢你找到这个问题。 - Jesse
1
请注意短语“类Resource的所有方法(包括finalize)都被包含在synchronized (this)代码块中。”这是一个关键因素,该类具有非平凡的finalize方法,并使其synchronized建立了最后使用和终止之间的* happens-before *​​关系。它还说:“此外,这样的代码块不得包括无限循环或自身无法访问,这是对'通常情况下'免责声明的特殊情况异常。” 这表明短语“通常情况下”可能会导致混淆,因为它容易被误解为“总是”。 - Holger

0

sleep方法不会离开同步块,因此不会释放锁或监视器对象,它只是阻塞执行一段时间。由于锁从未被释放,垃圾收集器将不会收集它,除非持有它的线程完成同步块的执行或使用wait()方法释放它。

因此,如果thread1()有足够的时间完成getMonitorObject()调用,则对于thread2()来说,它是保证非空的。

更新:啊哈!现在我看到了弱引用。我真傻!

是的,弱引用自动符合垃圾回收条件。但它位于类的范围内,因此可以说并不那么弱。除非类x的实例在范围内,否则它将不符合垃圾回收条件。因此,在这种情况下,它是保证非空的


你有这个来源吗:“由于锁从未被释放,垃圾收集器将不会收集它,除非持有完成执行的线程”? - Jesse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接