ReentrantLock 在同一个线程中被神秘地解锁,即使没有其他线程正在访问它

Posted

技术标签:

【中文标题】ReentrantLock 在同一个线程中被神秘地解锁,即使没有其他线程正在访问它【英文标题】:ReentrantLock gets mysteriously unlocked within the same thread, even while no other threads are accessing it 【发布时间】:2019-08-11 02:24:15 【问题描述】:

我有一些我认为非常简单的代码:

public int internalWrite(byte[] data, int offset, int size) throws InterruptedException 
    lock.lockInterruptibly();
    try 
        if (state == State.RELEASED) throw new TrackReleasedException();
        return track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING);
     finally 
        if (!lock.isHeldByCurrentThread()) 
            Log.e("phonographnative", "internalWrite() lock is not held by current thread! " + Thread.currentThread());
         else lock.unlock();
    

lockfair 可重入锁,但非fair 也会出现此问题。 trackandroid AudioTrack;它的write 方法主要是本机代码(但与线程无关)。它无论如何都无法访问lock。在实践中实际上从未抛出异常(在调查此行为时也从未抛出异常)。发生的情况是,非常可重复地(稍后会详细介绍),锁将在同一个线程中神秘地解锁,从而导致出现日志消息。以前,当我没有进行此检查时,预计会抛出 IllegalMonitorStateException。在这种情况发生几次之后,在锁的代码中就会出现java.lang.AssertionError: Attempt to repark。一些示例性的日志输出:

2019-03-20 12:20:37.428 8097-8181/com.kabouzeid.gramophone.debug E/phonographnative: internalWrite() lock is not held by current thread! Thread[phonographnative-decoding-65308.0,5,main]
2019-03-20 12:20:37.428 8097-8184/com.kabouzeid.gramophone.debug E/phonographnative: internalWrite() lock is not held by current thread! Thread[phonographnative-decoding-65308.0,5,main]
2019-03-20 12:20:37.428 8097-8181/com.kabouzeid.gramophone.debug E/phonographnative: internalWrite() lock is not held by current thread! Thread[phonographnative-decoding-65308.0,5,main]
2019-03-20 12:20:37.428 8097-8184/com.kabouzeid.gramophone.debug E/phonographnative: internalWrite() lock is not held by current thread! Thread[phonographnative-decoding-65308.0,5,main]
2019-03-20 12:20:37.430 8097-8184/com.kabouzeid.gramophone.debug E/AndroidRuntime: FATAL EXCEPTION: phonographnative-decoding-65308.0
    Process: com.kabouzeid.gramophone.debug, PID: 8097
    java.lang.AssertionError: Attempt to repark
        at java.lang.Thread.parkFor$(Thread.java:2143)
        at sun.misc.Unsafe.park(Unsafe.java:325)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
        at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:312)
        at com.kabouzeid.gramophone.service.ffmpeg.AudioContext.internalWrite(AudioContext.java:197)
        at com.kabouzeid.gramophone.service.ffmpeg.AudioContext.write(AudioContext.java:177)
        at com.kabouzeid.gramophone.service.ffmpeg.FFmpegPlayer.decodeAndPlayAudio(Native Method)
        at com.kabouzeid.gramophone.service.ffmpeg.FFmpegPlayer.lambda$new$0(FFmpegPlayer.java:72)
        at com.kabouzeid.gramophone.service.ffmpeg.-$$Lambda$FFmpegPlayer$MKAlsDZBJzprKYoChfgA-0JlIi8.run(lambda)
        at java.lang.Thread.run(Thread.java:761)

即使没有其他线程尝试运行此代码,并且即使我注释掉所有其他尝试锁定/解锁此锁(由其他线程在不同的代码部分中),也会发生这种情况。这个问题很少会间歇性地发生,但当我尝试取消暂停正在写入的 AudioTrack 时会可靠地发生。最初写入它或执行其他任何操作(例如从头开始播放)时都不会发生这种情况。这种取消暂停发生在一个完全不同的线程上,我无法确定这两件事之间的因果关系。这可能只是一些随机调度程序的疯狂。

internalWrite 方法被非常频繁地调用,大约每秒数千次。我感觉这只是 Android 的 ReentrantLock 实现中的一个错误,因为被命中的断言完全在 JVM 代码中。 (“尝试重新停车”是什么意思?)但我不能排除我在自己的代码中遗漏了一些其他细节,如果对此有任何想法,我将不胜感激!

完整代码可以在here找到。我还查到了Android代码中的相关部分:AudioTrack#write、native_write_byte(方法叫法不同)、writeToTrack、AudioTrack->write。但是,他们根本没有帮助我阐明这个错误。

【问题讨论】:

有趣的错误。我建议尝试更多地了解锁认为它属于哪个线程 - 使用调试器或通过子类化ReentrantLock 以公开getOwner()。然后我会尝试使用条件断点来捕捉状态变化的时刻。 我曾经尝试过,似乎锁只是被完全解锁,而不是由特定线程拥有(getOwner 返回null)。我也尝试了断点,但我无法在 JVM 代码中设置语句断点,并且使用方法断点将性能降低到完全无法使用的程度。 你应该将lock设为私有。如果您有理由不将其设为私有,那么这可能就是问题所在。 @meew0 好吧,如果你想进一步探究这个问题,你可以复制 ReentrantLock 的源代码——称之为 ReentrantLock2,然后使用那个类。您可以根据需要设置断点,并添加自定义调试代码。甚至可能导致行为改变。 @MattTimmermans 我明白你的意思,但即使我删除 all 对锁的其他引用,问题仍然存在,所以这不是问题。 【参考方案1】:

    如果您能够在track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING); 中删除lock.unlock()

    那么您当前的代码将可以正常工作,无需进行任何修改。

(或)

    如果您无法在track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING); 中删除lock.unlock()

    当前代码需要使用两次获取锁进行调整:

    lock.lockInterruptibly();
    lock.lockInterruptibly();
    
public int internalWrite(byte[] data, int offset, int size) throws InterruptedException 
    lock.lockInterruptibly();
    lock.lockInterruptibly();
    try 
        if (state == State.RELEASED) throw new TrackReleasedException();
        return track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING);
     finally 
        if (!lock.isHeldByCurrentThread()) 
            Log.e("phonographnative", "internalWrite() lock is not held by current thread! " + Thread.currentThread());
         else lock.unlock();
    

【讨论】:

【参考方案2】:

会发生什么:

    线程 (A) 获取 lock 并开始工作。 另一个线程 (B) 尝试获取 lock 并被阻止,因为 A 尚未在 lock 上调用 unlock()。 B 被另一个线程 (C) 中断。 C 不必知道变量lock。 B 的阻塞被终止,因为它试图中断地获取锁。 因此finally-block 由B 执行。B 没有获取lock,因为A 没有在中断发生时释放它。因此,lock.isHeldByCurrentThread() 是 B 的 false

不要假设在finally 块中的if (!lock.isHeldByCurrentThread()) 的情况下是错误的或B 的不需要的状态,您可以简单地继续而不记录错误/警告,也可以不unlock() lock 在这种情况下。

public int internalWrite(byte[] data, int offset, int size) throws InterruptedException 
    try 
        lock.lockInterruptibly(); // if outside the try block the next lines would be executed in the described situation without having acquired the lock
        if (state == State.RELEASED) throw new TrackReleasedException();
        return track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING); 
     finally 
        if (lock.isHeldByCurrentThread()) 
            lock.unlock();
        
    

【讨论】:

【参考方案3】:

实际上,您的代码可能会在内部调用时释放 Lock / lock.unlock() track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING);

当您在 finally 块中第二次尝试释放锁 /lock.unlock() 时,它与当前线程没有任何锁,因此它会根据您的代码逻辑抛出错误。

如果您要在track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING); 中释放锁定/lock.unlock(),请检查您的代码

如果您无法删除 track.write(data, offset, size, AudioTrack.WRITE_NON_BLOCKING); 中的 lock.unlock()

然后您执行“DOUBLE LOCK”/获得两次锁定然后释放或解锁两次您的问题将得到解决...

便于理解的示例代码:

import java.util.concurrent.locks.ReentrantLock;
public class Main

    private final static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) 
        try 
        System.out.println("Hello World:::::"+internalWrite());
       catch(InterruptedException ie) 
          ie.printStackTrace();
      
    
    
    public static int internalWrite() throws InterruptedException
      lock.lockInterruptibly();
      lock.lockInterruptibly();// Comment this line to reproduce your issue
      try 
         if(lock.isHeldByCurrentThread())
         System.out.println("lock::::"+lock.isHeldByCurrentThread());
         lock.unlock();
         System.out.println("lock::::"+lock.isHeldByCurrentThread());
         return 1;
       finally 
          if (!lock.isHeldByCurrentThread()) 
            System.out.println("phonographnative internalWrite() lock is not held by current thread! " + Thread.currentThread());
         else lock.unlock();
      
   

【讨论】:

以上是关于ReentrantLock 在同一个线程中被神秘地解锁,即使没有其他线程正在访问它的主要内容,如果未能解决你的问题,请参考以下文章

ReentrantLock重入锁同步线程源码学习

ReentrantLock (重入锁) 源码浅析

ReentrantLock源码分析

JUC——线程同步锁(ReentrantLock)

Java并发系列ReentrantLock源码分析

多线程编程-- part5.1 互斥锁ReentrantLock