如何确定对象是否被锁定(同步),以免在Java中阻塞?


80

我有一个进程A,它在内存中包含一个带有一组记录(recordA,recordB等的表)的表。

现在,此过程可以启动许多影响记录的线程,有时我们可以有2个线程尝试访问同一记录-必须拒绝这种情况。具体来说,如果一条记录被一个线程锁定,我希望另一个线程中止(我不想阻止或等待)。

目前,我正在执行以下操作:

synchronized(record)
{
performOperation(record);
}

但这引起了我的问题...因为在Process1执行操作时,如果Process2进入其中,则它阻塞/等待同步语句,而在Process1完成时,它执行操作。相反,我想要这样的东西:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

关于如何实现这一目标的任何线索?任何帮助将非常感激。谢谢,

Answers:


84

有一点要注意的是,即时你收到这样的信息,它已经过期。换句话说,您可能会被告知没有人拥有该锁,但是当您尝试获取它时,您将阻塞,因为另一个线程在支票和您尝试获取它之间取出了锁。

Brian是正确的选择Lock,但我认为您真正想要的是它的tryLock方法:

Lock lock = new ReentrantLock();
......
if (lock.tryLock())
{
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
}
else
{
    // Someone else had the lock, abort
}

您也可以花tryLock一些时间等待呼叫-因此,您可以尝试在十分之一秒的时间内获取它,然后在无法获取时放弃(例如)。

(我感到遗憾的是,就我所知,Java API没有提供与“ Monitor.NET ”类相同的“内置”锁定功能。然后,同样,还有很多在线程化方面,我在这两个平台上都不喜欢其他事情-例如,每个对象都可能有一个监视器!)


是。那是个很好的观点。我从字面上看了示例代码,而以上内容绝对是更可靠的实现
Brian Agnew

5
但是,如何对每条记录使用锁?当前记录存储在记录的哈希表中...所以我需要匹配的哈希哈希表?我试图确保我拥有尽可能多的并发性,因此,如果进程要访问应该没问题的recordC(如果仅recordB被锁定)-我使用全局LOCK,那么它基本上与锁定整个哈希表相同。...有意义吗?
2009年

@ Shaitan00:最简单的方法是在记录中锁定。基本上,您希望与每个记录关联一个锁-请将其放在对象中。
乔恩·斯基特

显然:)我假设我不需要管理解锁?意思是,当它退出.tryLock()的{}时,是否将立即自动将其正确解锁?
2009年

1
您确实需要管理解锁。请参阅我在答案中引用的教程以及finally {}块中的unlock()方法
Brian Agnew

24

看一下Java 5并发包中引入的Lock对象。

例如

Lock lock = new ReentrantLock()
if (lock.tryLock()) {
   try {
      // do stuff using the lock...
   }
   finally {
      lock.unlock();
   }
}
   ...

ReentrantLock的对象基本上是在做同样的事情,传统synchronized机制,但有更多的功能。

编辑:正如乔恩所说,该isLocked()方法在那一刻告诉您,此后信息已过期。该的tryLock()方法会给更可靠的操作(注意,你可以用一个超时使用为好)

编辑2:tryLock()/unlock()为清楚起见,现在包括示例。



8

尽管使用Lock对象的上述方法是最好的方法,但是如果必须能够使用监视器检查锁定,则可以完成。但是,它确实带有健康警告,因为该技术不能移植到非Oracle Java VM上,并且由于它不受支持的公共API可能会在将来的VM版本中中断。

这是操作方法:

private static sun.misc.Unsafe getUnsafe() {
    try {
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void doSomething() {
  Object record = new Object();
  sun.misc.Unsafe unsafe = getUnsafe(); 
  if (unsafe.tryMonitorEnter(record)) {
    try {
      // record is locked - perform operations on it
    } finally {
      unsafe.monitorExit(record);
    }
  } else {
      // could not lock record
  }
}

我的建议是仅在无法重构代码以使用java.util.concurrent Lock对象并且在Oracle VM上运行时,才使用这种方法。


12
@alestanis,我不同意。今天,我在这里阅读这些答案,并对所有答案/评论感到高兴,无论何时给出它们。
汤姆(Tom)

@Tom这些答案由SO系统标记,这就是为什么我留下一条消息。当您开始审阅时,您会看到:)
alestanis 2012年

2
@alestanis,我认为这很不幸-我经常看到一些老问题,这些老问题都有一个可以接受的答案,但是也有更新,更好的答案,要么是因为技术变化,要么是因为接受的答案实际上并不完全正确。
汤姆(Tom)

1
除了该线程较旧且具有良好的答案之外,这里的答案也不是很好。如果您确实想使用监视器进行锁定,则可以使用Thread.holdsLock(object)。
Tobias 2014年

3

虽然Lock的答案非常好,但我认为我会发布使用其他数据结构的替代方法。本质上,您的各个线程都想知道哪些记录已锁定,哪些未锁定。一种方法是跟踪锁定记录,并确保数据结构具有正确的原子操作以将记录添加到锁定集中。

我将使用CopyOnWriteArrayList作为示例,因为它在说明中不太“神奇”。CopyOnWriteArraySet是更合适的结构。如果平均有很多记录同时锁定,那么这些实现可能会对性能产生影响。正确同步的HashSet也可以工作,并且锁很短。

基本上,用法代码如下所示:

CopyOnWriteArrayList<Record> lockedRecords = ....
...
if (!lockedRecords.addIfAbsent(record))
    return; // didn't get the lock, record is already locked

try {
    // Do the record stuff
}        
finally {
    lockedRecords.remove(record);
}

它使您不必管理每条记录的锁,并提供了一个单独的位置,以便出于某种原因需要清除所有锁。另一方面,如果您的记录数量很少,则具有同步功能的真实HashSet可能会做得更好,因为添加/删除查询将是O(1)而不是线性的。

只是一种不同的看待事物的方式。只取决于您实际的线程需求是什么。就个人而言,我会使用Collections.synchronizedSet(new HashSet()),因为它确实非常快……唯一的含义是,线程可能会在原本没有的情况下产生。


如果您想完全控制,可以将其放在自己的Lock类中。
bvdb

1

另一个解决方法是(如果您没有机会获得此处给出的答案)是使用超时。即低于1将在悬挂1秒后返回null:

ExecutorService executor = Executors.newSingleThreadExecutor();
        //create a callable for the thread
        Future<String> futureTask = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return myObject.getSomething();
            }
        });

        try {
            return futureTask.get(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            //object is already locked check exception type
            return null;
        }

-2

谢谢,这帮助我解决了比赛条件。我稍微改变了一下,以同时佩戴皮带和吊带。

因此,这是我对已接受答案的改进建议:

您可以tryLock()通过执行以下操作来确保对方法的安全访问:

  Lock localLock = new ReentrantLock();

  private void threadSafeCall() {
    boolean isUnlocked = false;

    synchronized(localLock) {
      isUnlocked = localLock.tryLock();
    }

    if (isUnlocked) {
      try {
        rawCall();
      }
      finally {
        localLock.unlock();
      }
    } else {
      LOGGER.log(Level.INFO, "THANKS! - SAVED FROM DOUBLE CALL!");
    }
  }

这样可以避免这样的情况:您tryLock()几乎同时会打两个电话,从而使返回可能会充满疑问。我想现在如果我错了,我可能在这里过分谨慎。但是,嘿!我的演出现在很稳定:-) ..

在我的博客上阅读有关我的发展问题的更多信息。


1
在锁对象上使用同步非常非常糟糕。锁定的全部目的是避免同步,因此您可能会超时或在无法获得锁定时快速返回。
Ajax
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.