【问题标题】:Synchronizing on an Integer value [duplicate]在整数值上同步 [重复]
【发布时间】:2009-03-18 20:15:47
【问题描述】:

可能重复:
What is the best way to increase number of locks in java

假设我想基于一个整数 id 值进行锁定。在这种情况下,有一个函数可以从缓存中提取一个值,如果该值不存在,则执行相当昂贵的检索/存储到缓存中。

现有代码未同步,可能会触发多个检索/存储操作:

//psuedocode
public Page getPage (Integer id){
   Page p = cache.get(id);
   if (p==null)
   {
      p=getFromDataBase(id);
      cache.store(p);
   }
}

我想做的是同步 id 上的检索,例如

   if (p==null)
   {
       synchronized (id)
       {
        ..retrieve, store
       }
   }

不幸的是,这不起作用,因为 2 个单独的调用可以具有相同的 Integer id 值但不同的 Integer 对象,因此它们不会共享锁,并且不会发生同步。

是否有一种简单的方法可以确保您拥有相同的 Integer 实例?例如,这是否可行:

 syncrhonized (Integer.valueOf(id.intValue())){

Integer.valueOf() 的 javadoc 似乎暗示您可能会得到相同的实例,但这看起来不像是保证:

返回一个整数实例 表示指定的 int 值。 如果一个新的 Integer 实例不是 需要,这种方法一般应该 优先使用 构造函数 Integer(int),如 this 方法可能会产生 明显更好的空间和时间 通过频繁缓存来提高性能 请求的值。

那么,除了更复杂的解决方案(例如保持锁定对象的 WeakHashMap 与 int 键控)之外,关于如何获得保证相同的 Integer 实例的任何建议? (这并没有错,似乎肯定有一个明显的单行比我错过了)。

【问题讨论】:

标签: java synchronization


【解决方案1】:

您真的不想在Integer 上进行同步,因为您无法控制哪些实例相同以及哪些实例不同。 Java 只是没有提供这种跨不同 JVM 可靠的工具(除非您在小范围内使用整数)。如果您确实必须在 Integer 上进行同步,那么您需要保留一个 Map 或 Set of Integer,这样您就可以保证获得您想要的确切实例。

最好创建一个新对象,可能存储在HashMap 中,该对象由Integer 键入,以进行同步。像这样的:

public Page getPage(Integer id) {
  Page p = cache.get(id);
  if (p == null) {
    synchronized (getCacheSyncObject(id)) {
      p = getFromDataBase(id);
      cache.store(p);
    }
  }
}

private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>();

private Object getCacheSyncObject(final Integer id) {
  locks.putIfAbsent(id, id);
  return locks.get(id);
}

为了解释这段代码,它使用ConcurrentMap,它允许使用putIfAbsent。你可以这样做:

  locks.putIfAbsent(id, new Object());

但随后您会产生(少量)为每次访问创建对象的成本。为了避免这种情况,我只是将整数本身保存在Map 中。这能达到什么目的?为什么这与仅使用 Integer 本身有什么不同?

当您从Map 执行get() 时,会将密钥与equals() 进行比较(或者至少使用的方法相当于使用equals())。相同值的两个不同 Integer 实例将彼此相等。因此,您可以将“new Integer(5)”的任意数量的不同 Integer 实例作为参数传递给 getCacheSyncObject,并且您将始终只返回包含该值的第一个传递的实例。

您可能不想在Integer 上进行同步是有原因的……如果多个线程在Integer 对象上进行同步,您可能会陷入死锁,从而在他们想要使用不同的锁时无意中使用了相同的锁.您可以使用

  locks.putIfAbsent(id, new Object());

版本,因此每次访问缓存都会产生(非常)小的成本。这样做,您保证此类将在没有其他类将同步的对象上进行同步。总是一件好事。

【讨论】:

  • 实施者必须扩展此解决方案以同步锁映射并添加某种引用计数或释放机制。
  • 是的,如果某个项目从页面缓存中释放,那么您想要释放用于该页面缓存项目的锁。
  • getCacheSyncObject 内部是否存在竞争?同步是否对调用结果或其他对象生效以计算表达式的值?有一个开口可能最终会在 2 个不同的对象上同步。另一个解决方案中的 putIfAbsent 可能会起作用。
  • 这张地图不是无限增长的吗?从 finally 块内同步块末尾的映射中删除锁定对象条目是否有意义?
  • @SergeiLedvanov:是的,这是 OP 代码中的竞赛。使此代码线程安全需要更改 getPage()。一种选择是将Page p = cache.get(id);if 移动到synchronized (getCacheSyncObject(id)) 块内。你总是要付出同步的代价……但你会是线程安全的。
【解决方案2】:

Integer.valueOf() 仅返回有限范围内的缓存实例。你没有指定你的范围,但一般来说,这不起作用。

但是,我强烈建议您不要采用这种方法,即使您的值在正确的范围内。由于这些缓存的Integer 实例可用于任何代码,因此您无法完全控制同步,这可能会导致死锁。这与人们试图锁定 String.intern() 的结果相同。

最好的锁是私有变量。由于只有您的代码可以引用它,因此您可以保证不会发生死锁。

顺便说一句,使用WeakHashMap 也不起作用。如果作为键的实例未被引用,它将被垃圾回收。如果是强引用,可以直接使用。

【讨论】:

    【解决方案3】:

    使用线程安全的映射,例如ConcurrentHashMap。这将允许您安全地操作地图,但使用不同的锁来进行真正的计算。通过这种方式,您可以在一张地图上同时运行多个计算。

    使用ConcurrentMap.putIfAbsent,但不是放置实际值,而是使用Future 和计算量轻的构造。可能是FutureTask 实现。运行计算,然后get 结果,这将线程安全地阻塞直到完成。

    【讨论】:

      【解决方案4】:

      在 Integer 上使用同步听起来设计上确实是错误的。

      如果您只需要在检索/存储期间单独同步每个项目,您可以创建一个 Set 并将当前锁定的项目存储在那里。换句话说,

      // this contains only those IDs that are currently locked, that is, this
      // will contain only very few IDs most of the time
      Set<Integer> activeIds = ...
      
      Object retrieve(Integer id) {
          // acquire "lock" on item #id
          synchronized(activeIds) {
              while(activeIds.contains(id)) {
                  try { 
                      activeIds.wait();   
                  } catch(InterruptedExcption e){...}
              }
              activeIds.add(id);
          }
          try {
      
              // do the retrieve here...
      
              return value;
      
          } finally {
              // release lock on item #id
              synchronized(activeIds) { 
                  activeIds.remove(id); 
                  activeIds.notifyAll(); 
              }   
          }   
      }
      

      商店也是如此。

      底线是:没有一行代码可以完全按照您需要的方式解决此问题。

      【讨论】:

      • +1 我基本上喜欢这种方法。我在下面的帖子中提到了一个潜在问题,即在高争用情况下,当任何一个 ID 可用时,所有 ID 的等待线程都会被唤醒。
      • 如果您没有太多线程,这可能不是一个真正的问题。但如果是这种情况,您可以使用 Object.notify() 并使用 Object.wait(maxTime) 只是为了确保没有调用者被卡住。
      • 同意在低竞争下可能不是太大的问题。不幸的是,我认为你提到的解决方案实际上可能更糟......!
      • 请记住,“真正的 while(){wait...} 争用”仅发生在获取锁时,而不是在加载/存储期间。这意味着如果您有 100 个线程同时尝试获取锁,那么您无论如何都会发生争用
      • 另外,maxTime 值只是为了避免潜在的问题。不过,我看到了一个不同的问题:获取锁缺乏公平性。这真的可能是一个问题,而不是你提到的,恕我直言。我承认,一个通用的解决方案(适用于所有场景)需要完全重写。
      【解决方案5】:

      以 Integer 对象为键的 ConcurrentHashMap 怎么样?

      【讨论】:

      • 是的,正如我所提到的,我可以使用某种映射 对象将是私有锁。我更感兴趣的是找出 java 中是否有某种方法可以保证特定的 Integer 实例。
      【解决方案6】:

      您可以查看 this code 以从 ID 创建互斥锁。代码是为字符串 ID 编写的,但可以很容易地为整数对象编辑。

      【讨论】:

        【解决方案7】:

        从各种答案中可以看出,给这只猫剥皮的方法有很多种:

        • Goetz 等人保留 FutureTasks 缓存的方法在这种情况下非常有效,在这种情况下,您“无论如何都要缓存一些东西”,所以不要介意构建 FutureTask 对象的映射(如果您确实介意映射增长,至少可以很容易地同时修剪它)
        • 作为对“如何锁定 ID”的一般回答,Antonio 概述的方法的优势在于,在添加/删除锁定映射时很明显。

        您可能需要注意 Antonio 的 实现 的潜在问题,即 notifyAll() 将在 one 时唤醒等待 所有 ID 的线程 其中一些变得可用,在高争用情况下可能无法很好地扩展。原则上,我认为您可以通过为每个当前锁定的 ID 设置一个 Condition 对象来解决这个问题,这就是您等待/发出信号的东西。当然,如果实际上在任何给定时间等待的 ID 很少超过一个,那么这不是问题。

        【讨论】:

          【解决方案8】:

          史蒂夫,

          您提出的代码在同步方面存在大量问题。 (安东尼奥也一样)。

          总结一下:

          1. 您需要缓存一个昂贵的 对象。
          2. 您需要确保在一个线程进行检索时,另一个线程不会尝试检索相同的对象。
          3. 对于所有试图获取对象的 n 线程,只有 1 个对象被检索并返回。
          4. 这适用于请求它们不相互竞争的不同对象的线程。

          实现这一点的伪代码(使用 ConcurrentHashMap 作为缓存):

          ConcurrentMap<Integer, java.util.concurrent.Future<Page>> cache = new ConcurrentHashMap<Integer, java.util.concurrent.Future<Page>>;
          
          public Page getPage(Integer id) {
              Future<Page> myFuture = new Future<Page>();
              cache.putIfAbsent(id, myFuture);
              Future<Page> actualFuture = cache.get(id);
              if ( actualFuture == myFuture ) {
                  // I am the first w00t!
                  Page page = getFromDataBase(id);
                  myFuture.set(page);
              }
              return actualFuture.get();
          }
          

          注意:

          1. java.util.concurrent.Future 是一个接口
          2. java.util.concurrent.Future 实际上没有 set() 而是查看实现 Future 的现有类以了解如何实现自己的 Future(或使用 FutureTask)
          3. 将实际检索推送到工作线程几乎肯定是个好主意。

          【讨论】:

            【解决方案9】:

            请参阅Java Concurrency in Practice 中的第 5.6 节:“构建高效、可扩展的结果缓存”。它处理您要解决的确切问题。尤其要查看 memoizer 模式。


            (来源:umd.edu

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2021-10-23
              • 2021-12-06
              • 2012-03-02
              • 2013-04-20
              • 2015-03-23
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多