Java synchronized块使用方法调用来获取synch对象

时间:2010-11-09 14:49:08

标签: java concurrency synchronized

我们正在编写一些锁定代码并遇到了一个特殊的问题。我们使用ConcurrentHashMap来获取我们锁定的Object实例。所以我们的synchronized块看起来像这样

synchronized(locks.get(key)) { ... }

我们已经重写了ConcurrentHashMap的get方法,使它总是返回一个新对象,如果它没有包含该对象的那个。

@Override
public Object get(Object key) {
   Object o = super.get(key);
   if (null == o) {
      Object no = new Object();
      o = putIfAbsent((K) key, no);
      if (null == o) {
         o = no;
      }
   }
   return o;
}

但是有一个get-method返回对象的状态,但是线程还没有进入synchronized块。允许其他线程获取相同的对象并锁定它。

我们有潜在的竞争条件

  • 主题1:使用键A获取对象,但不进入同步块
  • 线程2:使用密钥A获取对象,进入同步块
  • 主题2:从地图中删除对象,退出synchronized块
  • 主题1:使用不再位于地图中的对象进入同步块
  • 线程3:获取密钥A的新对象(与线程1不同的对象)
  • 线程3:进入同步块,而线程1也使用密钥A
  • 进入同步块

如果java在返回get调用后直接进入synchronized块,则无法实现这种情况。如果没有,是否有人对我们如何删除密钥有任何意见而不必担心这种竞争条件?

5 个答案:

答案 0 :(得分:3)

正如我所看到的,问题源于您锁定地图值这一事实,而实际上您需要锁定(或某些派生的它)。如果我理解正确,您希望避免两个线程使用相同的密钥运行临界区。

你可以锁上钥匙吗?你能保证你总是使用相同的密钥实例吗?

一个不错的选择:

根本不要删除锁。使用值较弱的ReferenceMap。这样,只有当任何线程当前未使用映射条目时,才会删除映射条目。

注意:

1)现在你必须同步这个地图(使用Collections.synchronizedMap(..))。

2)您还需要同步生成/返回给定键值的代码。

答案 1 :(得分:1)

代码原样是线程安全的。话虽这么说,如果你要从CHM中删除,那么在从集合返回的对象上进行同步时所做的任何类型的假设都将丢失。

  

但是有没有一个州   get-method返回了对象,   但线程尚未进入   同步块。允许其他   线程来获取相同的对象和   锁定它。

是的,但是只要您在对象上进行同步,就会发生这种情况。夸张的是,另一个线程在另一个线程存在之前不会进入同步块。

  

如果没有,是否有人有任何输入   如何在没有的情况下删除密钥   不得不担心这场比赛   条件?

确保这种原子性的唯一真正方法是在CHM或其他对象上进行同步(由所有线程共享)。最好的方法是不要从CHM中删除。

答案 2 :(得分:1)

你有两个选择:

一个。您可以在同步块内查看一次地图。

Object o = map.get(k);
synchronized(o) {
  if(map.get(k) != o) {
    // object removed, handle...
  }
}

湾您可以扩展您的值以包含指示其状态的标志。当从地图中删除一个值时,您设置一个标志,指示它已被删除(在同步块内)。

CacheValue v = map.get(k);
sychronized(v) {
  if(v.isRemoved()) {
    // object removed, handle...
  }
}

答案 3 :(得分:1)

感谢所有伟大的建议和想法,真的很感激!最后,这个讨论使我想出了一个不使用对象进行锁定的解决方案。

只是简要描述我们实际在做什么。

我们有一个缓存,可以从我们的环境中不断接收数据。缓存有几个“桶”用于每个密钥,并在它们进入时将聚合事件放入桶中。进入的事件有一个确定要使用的缓存条目的密钥,以及一个确定缓存条目中应该是的桶的时间戳。递增。

缓存还有一个定期运行的内部刷新任务。它将迭代所有缓存条目并刷新所有存储桶,但将当前存储桶刷新到数据库。

现在,传入数据的时间戳可以是过去的任何时间,但其中大部分都是针对最近的时间戳。因此,当前存储桶在之前的时间间隔内将获得比存储桶更多的命中。

知道这一点,我可以证明我们的竞争条件。所有这些代码都用于单个缓存条目,因为该问题与单个缓存元素的并发写入和刷新隔离。

// buckets :: ConcurrentMap<Long, AtomicLong>

void incrementBucket(long timestamp, long value) {
   long key = bucketKey(timestamp, LOG_BUCKET_INTERVAL);
   AtomicLong bucket = buckets.get(key);
   if (null == bucket) {
      AtomicLong newBucket = new AtomicLong(0);
      bucket = buckets.putIfAbsent(key, newBucket);
      if (null == bucket) {
          bucket = newBucket;
      }
   }

   bucket.addAndGet(value);
}

Map<Long, Long> flush() {
   long now = System.currentTimeMillis();
   long nowKey = bucketKey(now, LOG_BUCKET_INTERVAL);

   Map<Long, Long> flushedValues = new HashMap<Long, Long>();
   for (Long key : new TreeSet<Long>(buckets.keySet())) {
      if (key != nowKey) {
         AtomicLong bucket = buckets.remove(key);
         if (null != bucket) {
            long databaseKey = databaseKey(key);
            long n = bucket.get()
            if (!flushedValues.containsKey(databaseKey)) {
               flushedValues.put(databaseKey, n);
            } else {
               long sum = flushedValues.get(databaseKey) + n;
               flushedValues.put(databaseKey, sum);
            }
         }
      }
   }

   return flushedValues;
}

可能发生的事情是:(fl = flush thread,it = increment thread)

  • it:输入incrementBucket,直到调用addAndGet(value)之前执行
  • fl:进入flush并迭代桶
  • fl:到达正在递增的桶
  • fl:删除它并调用bucket.get()并将值存储到刷新值
  • 它:增加桶(现在将丢失,因为桶已被刷新并被移除)

解决方案:

void incrementBucket(long timestamp, long value) {
   long key = bucketKey(timestamp, LOG_BUCKET_INTERVAL);

   boolean done = false;
   while (!done) {
      AtomicLong bucket = buckets.get(key);
      if (null == bucket) {
         AtomicLong newBucket = new AtomicLong(0);
         bucket = buckets.putIfAbsent(key, newBucket);
         if (null == bucket) {
             bucket = newBucket;
         }
      }

      synchronized (bucket) {
         // double check if the bucket still is the same
         if (buckets.get(key) != bucket) {
            continue;
         }
         done = true;

         bucket.addAndGet(value);
      }
   }
}

Map<Long, Long> flush() {
   long now = System.currentTimeMillis();
   long nowKey = bucketKey(now, LOG_BUCKET_INTERVAL);

   Map<Long, Long> flushedValues = new HashMap<Long, Long>();
   for (Long key : new TreeSet<Long>(buckets.keySet())) {
      if (key != nowKey) {
         AtomicLong bucket = buckets.get(key);
         if (null != value) {
            synchronized(bucket) {
               buckets.remove(key);
               long databaseKey = databaseKey(key);
               long n = bucket.get()
               if (!flushedValues.containsKey(databaseKey)) {
                  flushedValues.put(databaseKey, n);
               } else {
                  long sum = flushedValues.get(databaseKey) + n;
                  flushedValues.put(databaseKey, sum);
               }
            }
         }
      }
   }

   return flushedValues;
}

我希望这对可能遇到同样问题的其他人有用。

答案 4 :(得分:0)

您提供的两个代码段 fine ,就像它们一样。你所做的与使用Guava的MapMaker.makeComputingMap()的懒惰实例可能有什么用,但我发现这些键被懒惰地创建的方式没有问题。

你是对的,因为在 {/ 1}}锁定对象的查找之后,完全有可能在进入之前 sychronized。

我的问题在于你的竞争条件描述中的第三个要点。你说:

  

线程2:从地图中删除对象,退出同步块

哪个对象,哪个地图?一般来说,我假设您正在查找要锁定的密钥,然后在同步块内的其他数据结构上执行其他操作。如果您正在谈论从一开始提到的ConcurrentHashMap中删除锁定对象,那就是大量差异。

真正的问题是这是否是必要的。在通用环境中,我不认为只记住所有已查找的键的锁定对象(即使这些键不再代表活动对象)也不会有任何内存问题。 很多更难以想出一些安全处理可能存储在某个其他线程的局部变量中的对象的方法,如果你想沿着这条路走下去我感觉性能会降低到密钥查找周围的单个粗锁的性能。

如果我误解了那里发生的事情,请随时纠正我。

编辑:好的 - 在这种情况下,我坚持我的上述主张,最简单的方法是不删除密钥;这可能实际上并不像你想象的那样有问题,因为空间增长的速度非常小。通过我的计算(很可能是关闭,我不是空间计算的专家,你的JVM可能会有所不同),地图增长了大约14Kb /小时。在此地图耗尽100MB的堆空间之前,您必须有一年的连续正常运行时间。

但我们假设确实需要删除密钥。这会产生一个问题,即在您知道没有线程正在使用它之前,您无法删除密钥。这会导致鸡蛋和鸡蛋问题,你需要所有线程在 else 上进行同步,以便获得(检查)原子性和跨线程的可见性,这意味着你可以'除了在整个事物周围打一个同步块之外,还要做很多事情,完全颠覆你的锁定条带策略。

让我们重温一下约束。这里的主要内容是事情最终被清理 。这不是正确性约束,而只是内存问题。因此,我们真正想要做的是确定密钥肯定不再使用的某个点,然后使用它作为触发器将其从地图中删除。这里有两种情况:

  1. 您可以识别出这样的情况,并对其进行逻辑测试。在这种情况下,您可以使用(在最坏的情况下)某种计时器线程从地图中删除键,或者希望将某些逻辑与您的应用程序更加干净地集成。
  2. 您无法确定知道任何不再使用密钥的情况。在这种情况下,根据定义,没有必要从地图中删除密钥是安全的。所以事实上,为了正确起见,你必须把它们留在里面。
  3. 无论如何,这实际上归结为手动垃圾收集。当你可以懒洋洋地确定它们不再被使用时,从地图中删除键。你现在的解决方案太急切了,因为(正如你所指出的那样)它在这种情况发生之前正在进行删除。