同步整数值

时间:2009-03-18 20:15:48

标签: java synchronization

  

可能重复:
  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似乎暗示您可能会获得相同的实例,但这看起来不像是保证:

  

返回一个Integer实例   表示指定的int值。   如果没有新的Integer实例   要求,这种方法一般应该   优先使用   构造函数Integer(int),就像这样   方法很可能会产生   明显更好的空间和时间   经常缓存的性能   要求的价值。

那么,关于如何获得保证相同的Integer实例的任何建议,除了更精细的解决方案,比如保持Lock对象的WeakHashMap键入int? (没有错,似乎必须有一个明显的单行而不是我错过了。)

9 个答案:

答案 0 :(得分:49)

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

最好是创建一个新对象,可能存储在由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本身有什么不同?

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

有些原因可能导致您不希望在Integer上进行同步...如果多个线程正在Integer个对象上进行同步,您可能会陷入死锁,因此无意中使用了相同的锁定使用不同的锁。您可以使用

来解决此风险
  locks.putIfAbsent(id, new Object());

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

答案 1 :(得分:4)

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

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

答案 2 :(得分:3)

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

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

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

顺便说一句,使用WeakHashMap也无效。如果作为密钥的实例未被引用,则将对其进行垃圾回收。如果它被强烈引用,你可以直接使用它。

答案 3 :(得分:3)

在整数上使用synchronized听起来确实是错误的设计。

如果您只需要在检索/存储期间单独同步每个项目,您可以创建一个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(); 
        }   
    }   
}

同样的商店。

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

答案 4 :(得分:1)

ConcurrentHashMap如何将Integer对象作为键?

答案 5 :(得分:1)

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

答案 6 :(得分:1)

参见Java Concurrency in Practice中的5.6节:“构建高效,可扩展的结果缓存”。它涉及您要解决的确切问题。特别是,请查看memoizer模式。

alt text
(来源:umd.edu

答案 7 :(得分:1)

从各种答案中可以看出,有很多方法可以让这只猫受到伤害:

  • Goetz等人保留FutureTasks缓存的方法在这样的情况下非常有效,你无论如何都要“缓存”,所以不要介意构建FutureTask对象的地图(如果你确实想到了地图的增长) ,至少很容易同时修剪它)
  • 作为“如何锁定身份证”的一般答案,安东尼奥概述的方法具有以下优点:当锁的地图被添加到/从中移除时显而易见。

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

答案 8 :(得分:1)

史蒂夫,

您提出的代码在同步方面存在许多问题。 (安东尼奥也是如此)。

总结:

  1. 你需要缓存一个昂贵的 对象。
  2. 您需要确保在一个线程正在进行检索时,另一个线程也不会尝试检索同一个对象。
  3. 对于所有试图获取对象的n-threads,只检索并返回该对象。
  4. 对于请求不同对象的线程,它们不会相互竞争。
  5. 使用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. 将实际检索推送到工作线程几乎肯定是个好主意。