在Java中释放Semaphore对象的正确方法是什么?

3
这是大多数在线资源中写的代码。但这个代码不正确,因为考虑一种情况:当一个线程被阻塞时,然后被中断。即使它没有获取锁,该线程仍将释放锁。这是不正确的。那么在Java中释放信号量的正确实现是什么?
Semaphore lock = new Semaphore(1);

  add(){
    try {
        lock.acquire();

        // critical section

    } catch (InterruptedException e) {

          Thread.currentThread().interrupt();

    }finally{
       lock.release()
    }
}

我认为这是正确的解决方案:-. 对吗?

try {
    lock.acquire();

    try {
        // do some stuff here
    } finally {

        lock.release();

    }
} catch(InterruptedException ie) {

    Thread.currentThread().interrupt();
    throw new RuntimeException(ie);

}

请问您能否发布此代码的其余部分?这样我们才能清楚地了解您正在尝试实现什么。 - Nathan Hughes
谢谢,现在清晰多了。 - Nathan Hughes
3
不要破坏自己的帖子,你可以删除它们。 - user207421
3个回答

4

信号量(semaphores)没有所有权的概念,许可证不是实际的物品,它只是信号量保持的计数。所以问题是,方法执行后计数是否正确?如果中断,您会带着一个可用许可或两个离开该方法?

Oracle网站上api文档中的信号量示例没有任何finally块,在很多情况下它们都不相关。

如果您使用此信号量来实现互斥锁,并且它只有一个许可,我希望它应该像使用锁一样具有try-finally块(来自于可重入锁(ReentrantLock) api文档):

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

如果代码吞掉了InterruptedException并让线程继续前进,那么在方法结束时计数是否正确变得重要起来,它可能会阻止其他调用获取许可。
我通常使用的一般模式是,在try块之前获取资源,然后在try块中使用它,并在finally块中关闭/释放/清理。这适用于IO、JDBC资源等等。你可以尝试避免这种方式,将获取放在try块中,然后在清理之前检查是否为null。你也可以尝试在finally块中做太多的事情,未能捕获关闭异常,从而造成资源泄漏。最好尽量减少finally块中的代码量。
一个正确的示例在http://jcip.net/listings/BoundedHashSet.java(来自书籍《Java Concurrency In Practice》)中。
public boolean add(T o) throws InterruptedException {
    sem.acquire();
    boolean wasAdded = false;
    try {
        wasAdded = set.add(o);
        return wasAdded;
    } finally {
        if (!wasAdded)
            sem.release();
    }
}

这段代码展示了一个带有finally的try块,它进行了一些清理工作(如果没有将任何内容添加到集合中,则释放许可),在进入该try块之前调用了acquire。
如果将此示例中的acquire调用移动到try块内部,则不会产生影响。我认为将调用放在try块上面更好,但在此示例中,它不会影响正确性,因为它使用标志来决定是否释放许可。
我会像jcip示例中那样使用标志,设置为true后获取,并且仅在设置了标志时才释放。这样可以将acquire放在try块内部。
    boolean wasAcquired = false;
    try {      
         sem.acquire();
        wasAcquired = true;
        // crit sect
     } catch (InterruptedException e) {
        Thread.currentThread.interrupt();
    } finally {
        if (wasAcquired)
            sem.release();
    }

或者考虑使用acquireUninterruptibly()。但是要想一想,如果这些调用不抛出InterruptedException,那么你的代码中哪部分确保了当收到中断请求时代码实际停止工作?看起来你可能会陷入一个无效的循环中,其中线程尝试获取、抛出InterruptedException、捕获它并设置中断状态,然后在下次线程尝试获取时再重复同样的事情,反复循环。抛出InterruptedException让你能够快速响应取消请求,同时确保清理工作在finally块中完成。

acquire() 抛出异常,因此必须在 try 块中使用。您能否发布正确的实现? - Karen delfino
1
这并不意味着它必须在同一个try块中。很可能InterruptedException甚至不应该被处理,而应该被抛出。 - Nathan Hughes
我想在代码本身中处理异常。 - Karen delfino
@Karen:你可以使用acquireUninterruptibly(),而不必去烦恼它。 - Nathan Hughes
非常感谢您的解释。 - Karen delfino

1

只需添加一个标志,指示锁是否已被获取:

boolean acquired = false;
try {
    lock.acquire();
    acquired = true;
    // critical section

} catch (InterruptedException e) {

     // do anything

} finally {
   if (acquired) {
       lock.release()
   }
}

另一种解决方案是使用Semaphore.acquireUninterruptibly()


0
线程即使未获取锁也会释放锁,这是不正确的。这是错误的。
并非如此。从Javadoc
没有要求释放许可证的线程必须通过调用acquire()来获取该许可证。在应用程序中的编程约定确立了信号量的正确使用方式。
这里没有问题需要解决。

Semaphore的Javadoc允许调用release(),但这并不意味着问题中的代码是正确的。如果acquire()被中断,它仍然会意外地增加许可证的数量,在大多数情况下这可能是一个错误。 - Philipp Wendler
EJP:假设你是我的投票者,请看看我更新的答案是否更少引起反感。 - Nathan Hughes
@PhilippWendler Javadoc允许这段代码,暗示信号量实现在其中运行时必须正确操作。 - user207421

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