同步块不会阻塞!

7

昨天我注意到了一些非常奇怪的事情。似乎有两个线程同时进入两个同步代码块,锁定了相同的对象。

包含相关代码的类(MyClass)类似于这样:

private static int[]    myLock  = new int[0];

protected static int methodA(final long handle, final byte[] sort) {
    synchronized (myLock) {
        return xsMethodA(handle, sort);
    }
}

protected static int methodB(final long handle) {
    synchronized (myLock) {
        return xsMethodB(handle);
    }
}

我创建了一个线程转储,针对运行上述类的应用程序,并且看到以下内容感到非常惊讶:
"http-8080-136" daemon prio=10 tid=0x00000000447df000 nid=0x70ed waiting for monitor entry [0x00007fd862aea000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.MyClass.methodA(MyClass.java:750)
    - locked <0x00007fd8a6b8c790> (a [I)
    at com.SomeOtherClass.otherMethod(SomeOtherClass.java:226)
    ...

"http-8080-111" daemon prio=10 tid=0x00007fd87d1a0000 nid=0x70c8 waiting for monitor entry [0x00007fd86e15f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.MyClass.methodB(MyClass.java:991)
    - locked <0x00007fd8a6b8c790> (a [I)
    at com.SomeOtherClass.yetAnotherMethod(SomeOtherClass.java:3231)
    ...

为了简化,我更改了类和方法名称,所以不要被愚蠢的名称所困惑。

似乎线程http-8080-136和http-8080-111都已经获取了myLock的锁。由于对象地址相同:0x00007fd8a6b8c790,因此它们是同一个对象。Java运行时规范对synchronized关键字有以下说明:

同步语句在执行线程的代表下获取互斥锁(§17.1),执行一个块,然后释放锁。当执行线程拥有锁时,没有其他线程可以获取该锁。[Java语言规范,14.19]

那么这怎么可能呢?

线程转储中还有另外44个“等待”锁。如果一个线程正在等待,情况就像这样:

"http-8080-146" daemon prio=10 tid=0x00007fd786dab000 nid=0x184b waiting for monitor entry [0x00007fd8393b6000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.MyClass.methodC(MyClass.java:750)
    - waiting to lock <0x00007fd8a6b8c790> (a [I)
    at com.SomeOtherClass.yetAnoterMethod2(SomeOtherClass.java:226)
4个回答

4
我在hotspot-dev邮件列表上提出了同样的问题,并从Christopher Phillips那里得到了非常好的答案:

嗨,Eduard

我认为这是线程转储误导了你。

如果您真的认为两个锁同时被锁定,那么您应该获取gcore(它是外部一致的)。

您看到的状态“等待监视器输入”实际上是MONITOR_WAIT,它可以表示以下代码在实际获取热锁之前的状态: (还请参见osThread.hpp中的OSThreadContendState),称为: src / share / vm / runtime / synchronizer.cpp

3413      OSThreadContendState osts(Self->osthread());
3414      ThreadBlockInVM tbivm(jt);
3415
3416      Self->set_current_pending_monitor(this);
3417
3418      // TODO-FIXME: change the following for(;;) loop to straight-line code.
3419      for (;;) {
3420        jt->set_suspend_equivalent();
3421        // cleared by handle_special_suspend_equivalent_condition()
3422        // or java_suspend_self()
3423
3424        EnterI (THREAD) ;
3425
3426        if (!ExitSuspendEquivalent(jt)) break ;
3427
3428        //
3429        // We have acquired the contended monitor, but while we were
3430        // waiting another thread suspended us. We don't want to enter
3431        // the monitor while suspended because that would surprise the
3432        // thread that suspended us.

克里斯


1
线程转储是如何进行的?如果线程没有暂停,锁所有权可能在转储一个线程和下一个线程之间发生了变化。

通过向进程发送QUIT信号。我不知道Sun VM在线程转储期间的行为。但我会假设进程被停止了。否则,您将获得不一致的线程转储。 - Eduard Wirch
我知道对于IBM JVM来说,这并不一定是真的,不确定SUN是否也是如此,但这绝对是需要记在心里的事情。 - Mike Tunnicliffe
这个网站声称所有线程都被暂停了:http://expertodev.wordpress.com/2009/05/30/how-to-take-java-thread-dump/ - Eduard Wirch

0

我认为相关信息是:"等待监视器条目",这对两个线程都是相同的。由于线程转储中标记了两个线程(守护线程),我猜测同时也有一个主线程在运行。可能主线程是当前监视器所有者,阻塞了另外两个线程吗?


不,主线程什么也不做。即使主线程持有锁,规范也没有区分主线程和守护线程。只允许一个线程拥有锁。 - Eduard Wirch
我同意,根据规范,只允许一个线程获得锁并进入关键部分。如线程转储所述,两个守护线程都在等待锁。您确定没有其他线程当前持有锁吗? - Javaguru
两个线程http-8080-136和http-8080-111正在持有锁。在线程转储中,还有另外44个线程“等待”该锁。 - Eduard Wirch
“持有锁”是什么意思?线程111和136被阻塞,因为它们正在尝试获取由另一个线程持有的锁。这44个线程都被阻塞了吗?它们都在尝试获取锁吗?还是这44个线程中有任何一个已经执行完毕了? - Javaguru
http-8080-136和http-8080-111的线程转储显示:已锁定<0x00007fd8a6b8c790>,因此它们已经获取了锁。剩下的44个线程的线程转储显示:等待锁定<0x00007fd8a6b8c790>。 - Eduard Wirch

0

他们还没有获取锁,否则您会在堆栈跟踪中看到xsMethodA或xsMethodB。


为什么http-8080-111和http-8080-146之间会有线程差异呢? - Eduard Wirch
同时,ThreadDumpAnalyzer将两个线程(http-8080-136、http-8080-111)均列为“锁定者”。 - Eduard Wirch
我的意思是,标题“synchronized doesn't block”是错误的。也许你的意思是“当没有人持有锁时,同步块会阻塞”? - Ha.
这是不正确的。两个线程正在持有锁。还有44个线程在等待锁。 - Eduard Wirch
你认为他们持有锁,但实际上并没有。否则,xsMethodA和xsMethodB几乎肯定会在堆栈跟踪中出现。 - Ha.

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