【问题标题】:How do determine if an object is locked (synchronized) so not to block in Java?如何确定一个对象是否被锁定(同步)以免在 Java 中阻塞?
【发布时间】:2023-03-26 05:12:01
【问题描述】:

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

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

目前我做这样的事情:

synchronized(record)
{
performOperation(record);
}

但这给我带来了问题......因为当 Process1 正在执行操作时,如果 Process2 进入它会阻塞/等待同步语句,并且当 Process1 完成时它会执行操作。相反,我想要这样的东西:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

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

【问题讨论】:

    标签: java synchronization


    【解决方案1】:

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

    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 没有为“内置”锁定提供与 .NET 中的 Monitor 类相同的功能。然后再说一次,在线程方面,我不喜欢这两个平台的许多其他东西——例如,每个对象都可能有一个监视器!)

    【讨论】:

    • 是的。那是个很好的观点。我从字面上理解了示例代码,而上面的代码绝对是一个更健壮的实现
    • 但是如何为每条记录使用一个锁呢?目前记录存储在记录的哈希表中......所以我需要一个匹配的锁哈希表?我试图确保我有尽可能多的并发性,所以如果一个进程想要访问应该没问题的recordC(如果只有recordB被锁定) - 我使用全局LOCK,那么它基本上与锁定整个哈希表相同。 ...这有什么意义吗?
    • @Shaitan00:最简单的方法是在记录中锁定。基本上你想要一个锁与每条记录相关联 - 所以把它放在对象中。
    • 显然 :) 我假设我不需要管理解锁?这意味着当它退出 .tryLock() 的 {} 时,它将自动并立即正确解锁?
    • 您确实需要管理解锁。请参阅我在答案中引用的教程和 finally{} 块中的 unlock() 方法
    【解决方案2】:

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

    例如

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

    ReentrantLock 对象与传统的synchronized 机制基本相同,但功能更多。

    编辑:正如 Jon 所指出的,isLocked() 方法会在那个瞬间告诉您,此后该信息已过时。 tryLock() 方法将提供更可靠的操作(请注意,您也可以将其与超时一起使用)

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

    【讨论】:

      【解决方案3】:

      我找到了这个,我们可以使用Thread.holdsLock(Object obj)来检查一个对象是否被锁定:

      当且仅当当前线程持有指定对象的监视器锁时返回true

      注意Thread.holdsLock() 返回false 如果锁被something 持有并且调用线程不是持有锁的线程。

      【讨论】:

        【解决方案4】:

        虽然使用 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 上运行时才使用此方法。

        【讨论】:

        • @alestanis,我不同意。我今天在这里阅读这些答案,并为所有答案/cmets 感到高兴,无论何时给出。
        • @Tom 这些答案被 SO 系统标记,这就是我留言的原因。当你开始复习时你会看到:)
        • @alestanis,我认为这很不幸 - 我经常看到旧问题的答案已被接受,但也有更新、更好的答案,因为技术发生了变化,或者因为接受的答案实际上并不完全正确。
        • 除了这个线程老了并且有一个很好的答案之外,这里的这个答案也不好。如果你真的想使用监视器锁定,你可以使用 Thread.holdsLock(object)。
        【解决方案5】:

        虽然 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 类我猜。
        【解决方案6】:

        另一种解决方法是(如果您没有机会获得此处给出的答案)是使用超时。即低于一个将在 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;
                }
        

        【讨论】:

          【解决方案7】:

          我还需要找到解决方案,因此搜索了 Java Concurrency API 并遇到了StampedLock。该项目使用 Java 8。 我正在使用与本机库通信并包含长期配置对象的重线程异步数据服务,有时需要复杂的并发逻辑;谢天谢地,这对于 StampedLock 类来说相对简单。

          StampedLock 有一个名为tryOptimisticRead 的方法,它不等待,它只是以长时间时间戳的形式返回状态,其中零 (0) 表示持有独占锁。然后我会延迟一秒钟,但您可以直接使用该功能而不会有任何延迟。

          这是我检测是否存在独占锁的方法,该范例用于多个位置并包括错误处理:

              int delayCount = 0;
          
              //Makes sure that if there is data being written to this field at
              // this moment, wait until the operation is finished writing the
              // updated data.
              while (data1StampedLock.tryOptimisticRead() == 0)
              {
                  try
                  {
                      delay(WRITE_LOCK_SHORT_DELAY);
                      delayCount += 1;
                  }
                  catch (InterruptedException e)
                  {
                      logError("Interrupted while waiting for the write lock to be
                                 released!", e);
                      Thread.currentThread().interrupt();
          
                      //There may be an issue with the JVM if this occurs, treat
                      // it like we might crash and try to release the write lock.
                      data1StampedLock.tryUnlockWrite();
                      break;
                  }
          
                  if (delayCount * WRITE_LOCK_SHORT_DELAY > TimeUnit.SECONDS.toMillis(1))
                  {
                      logWarningWithAlert("Something is holding a write lock on" +
                          " the data for a very, very long time (>1s). This may" +
                          " indicate a problem that could cause cascading" +
                          " problems in the near future." +
                          " Also, the value for the data that is about to be" +
                          " retrieved could potentially be invalid.");
                      break;
                  }
              }
          
              long nonExclusiveLockStamp = data1StampedLock.readLock();
              Data data1NonVolatile = data1;
              data1StampedLock.unlockRead(nonExclusiveLockStamp);
              
              return data1NonVolatile;
          

          StampedLock 上的读锁是非排他的,就像从线程安全的 Map 或 HashTable 中读取一样,它是多读/单写的。

          这是我如何使用排他锁与正在写入实例数据的其他线程进行通信:

              long d1LockStamp = data1StampedLock.writeLock();
              this.data1 = data1;
              data1StampedLock.unlockWrite(d1LockStamp);
          

          因此,如果您只想检查某物是否在任何给定时刻被锁定,您只需要像以下语句这样简单的东西来获取状态:

              boolean data1IsLocked = data1StampedLock.tryOptimisticRead() == 0;
          

          然后检查那个布尔值。

          当然,其他答案中提到的警告和Here Be Dragons信息(即信息立即过时),但如果你真的需要锁定某些东西并从另一个线程检查该锁定,这在我看来是最合理、最安全、最有效的使用java.util.concurrency包的方式,没有外部依赖。

          【讨论】:

            【解决方案8】:

            谢谢你,它帮助我解决了比赛条件。我稍微改了一下,既穿腰带又穿吊带。

            所以这是我对已接受答案的改进建议:

            您可以通过执行以下操作来确保您可以安全地访问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() 的情况,从而导致退货可能被怀疑已满。如果我错了,我想现在,我可能在这里过于谨慎了。但是,嘿!我的演出现在稳定了:-)..

            在我的Blog 上阅读有关我的开发问题的更多信息。

            【讨论】:

            • 在锁对象上使用同步是非常非常糟糕的。锁的全部意义在于避免同步,所以当你无法获得锁时,你可以超时或快速返回。
            猜你喜欢
            • 2013-01-16
            • 1970-01-01
            • 2015-12-21
            • 2017-07-25
            • 1970-01-01
            • 1970-01-01
            • 2012-02-08
            • 2011-03-04
            • 1970-01-01
            相关资源
            最近更新 更多