在Java中同步String对象

时间:2021-10-22 11:47:54

I have a webapp that I am in the middle of doing some load/performance testing on, particularily on a feature where we expect a few hundred users to be accessing the same page and hitting refresh about every 10 seconds on this page. One area of improvement that we found we could make with this function was to cache the responses from the web service for some period of time, since the data is not changing.

我有一个webapp,我正在进行一些负载/性能测试,特别是在我们希望几百个用户访问同一页面并在此页面上每10秒钟点击刷新的功能上。我们发现我们可以使用此功能进行改进的一个方面是在一段时间内缓存来自Web服务的响应,因为数据没有变化。

After implementing this basic caching, in some further testing I found out that I didn't consider how concurrent threads could access the Cache at the same time. I found that within the matter of ~100ms, about 50 threads were trying to fetch the object from the Cache, finding that it had expired, hitting the web service to fetch the data, and then putting the object back in the cache.

在实现这个基本缓存之后,在一些进一步的测试中,我发现我没有考虑并发线程如何同时访问Cache。我发现在大约100ms的时间内,大约有50个线程试图从Cache中获取对象,发现它已经过期,命中Web服务以获取数据,然后将对象放回缓存中。

The original code looked something like this:

原始代码看起来像这样:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  final String key = "Data-" + email;
  SomeData[] data = (SomeData[]) StaticCache.get(key);

  if (data == null) {
      data = service.getSomeDataForEmail(email);

      StaticCache.set(key, data, CACHE_TIME);
  }
  else {
      logger.debug("getSomeDataForEmail: using cached object");
  }

  return data;
}

So, to make sure that only one thread was calling the web service when the object at key expired, I thought I needed to synchronize the Cache get/set operation, and it seemed like using the cache key would be a good candidate for an object to synchronize on (this way, calls to this method for email b@b.com would not be blocked by method calls to a@a.com).

因此,为了确保当密钥对象到期时只有一个线程正在调用Web服务,我认为我需要同步Cache get / set操作,看起来使用缓存键似乎是一个很好的候选对象同步(这样,对电子邮件b@b.com的此方法的调用不会被方法调用a@a.com阻止)。

I updated the method to look like this:

我将方法更新为如下所示:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

I also added logging lines for things like "before synchronization block", "inside synchronization block", "about to leave synchronization block", and "after synchronization block", so I could determine if I was effectively synchronizing the get/set operation.

我还为“同步块之前”,“内部同步块”,“即将离开同步块”和“同步块之后”之类的内容添加了日志行,因此我可以确定是否有效地同步了get / set操作。

However it doesn't seem like this has worked. My test logs have output like:

然而,这似乎并没有起作用。我的测试日志输出如下:

(log output is 'threadname' 'logger name' 'message')
http-80-Processor253 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor253 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor253 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor253 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor263 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor263 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor263 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor131 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor131 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor131 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: inside synchronization block
http-80-Processor104 cache.StaticCache - get: object at key [SomeData-test@test.com] has expired
http-80-Processor104 cache.StaticCache - get: key [SomeData-test@test.com] returning value [null]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor283 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: about to enter synchronization block
http-80-Processor2 jsp.view-page - getSomeDataForEmail: inside synchronization block

(日志输出是'threadname''记录器名''消息')http-80-Processor253 jsp.view-page - getSomeDataForEmail:即将进入同步块http-80-Processor253 jsp.view-page - getSomeDataForEmail:内部同步块http -80-Processor253 cache.StaticCache - get:key at [SomeData-test@test.com]已过期http-80-Processor253 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor263 jsp.view-page - getSomeDataForEmail:即将进入同步块http-80-Processor263 jsp.view-page - getSomeDataForEmail:内部同步块http-80-Processor263 cache.StaticCache - get:object at key [SomeData -test@test.com]已过期http-80-Processor263 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor131 jsp.view-page - getSomeDataForEmail:即将到来输入同步块http-80-Processor131 jsp.view-page - getSomeDataForEmail:内部同步块http-80-Pro cessor131 cache.StaticCache - get:key at key [SomeData-test@test.com]已过期http-80-Processor131 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80 -Processor104 jsp.view-page - getSomeDataForEmail:内部同步块http-80-Processor104 cache.StaticCache - get:object at key [SomeData-test@test.com]已过期http-80-Processor104 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor252 jsp.view-page - getSomeDataForEmail:即将进入同步块http-80-Processor283 jsp.view-page - getSomeDataForEmail:即将进入同步块http-80-Processor2 jsp.view-page - getSomeDataForEmail:即将进入同步块http-80-Processor2 jsp.view-page - getSomeDataForEmail:内部同步块

I wanted to see only one thread at a time entering/exiting the synchronization block around the get/set operations.

我希望在get / set操作周围一次只能看到一个线程进入/退出同步块。

Is there an issue in synchronizing on String objects? I thought the cache-key would be a good choice as it is unique to the operation, and even though the final String key is declared within the method, I was thinking that each thread would be getting a reference to the same object and therefore would synchronization on this single object.

在String对象上同步是否存在问题?我认为缓存键是一个很好的选择,因为它对于操作是唯一的,即使在方法中声明了最终的String键,我也认为每个线程都会获得对同一个对象的引用,因此同步这个单个对象。

What am I doing wrong here?

我在这做错了什么?

Update: after looking further at the logs, it seems like methods with the same synchronization logic where the key is always the same, such as

更新:在进一步查看日志之后,似乎具有相同同步逻辑的方法,其中密钥始终相同,例如

final String key = "blah";
...
synchronized(key) { ...

do not exhibit the same concurrency problem - only one thread at a time is entering the block.

不会出现相同的并发问题 - 一次只有一个线程进入该块。

Update 2: Thanks to everyone for the help! I accepted the first answer about intern()ing Strings, which solved my initial problem - where multiple threads were entering synchronized blocks where I thought they shouldn't, because the key's had the same value.

更新2:感谢大家的帮助!我接受了关于intern()ing Strings的第一个答案,它解决了我的初始问题 - 多线程进入同步块,我认为它们不应该,因为密钥具有相同的值。

As others have pointed out, using intern() for such a purpose and synchronizing on those Strings does indeed turn out to be a bad idea - when running JMeter tests against the webapp to simulate the expected load, I saw the used heap size grow to almost 1GB in just under 20 minutes.

正如其他人已经指出的那样,使用intern()实现这一目的并同步这些字符串确实是一个坏主意 - 当针对webapp运行JMeter测试来模拟预期的负载时,我看到使用的堆大小增长到在不到20分钟的时间内将近1GB。

Currently I'm using the simple solution of just synchronizing the entire method - but I really like the code samples provided by martinprobst and MBCook, but since I have about 7 similar getData() methods in this class currently (since it needs about 7 different pieces of data from a web service), I didn't want to add almost-duplicate logic about getting and releasing locks to each method. But this is definitely very, very valuable info for future usage. I think these are ultimately the correct answers on how best to make an operation like this thread-safe, and I'd give out more votes to these answers if I could!

目前我正在使用只是同步整个方法的简单解决方案 - 但我真的很喜欢martinprobst和MBCook提供的代码示例,但是因为我目前在这个类中有大约7个类似的getData()方法(因为它需要大约7个不同的来自Web服务的数据片段,我不想添加几乎重复的逻辑来获取和释放每个方法的锁。但这对于未来的使用来说绝对是非常非常有价值的信息。我认为这些最终是关于如何最好地进行这种线程安全的操作的正确答案,如果可以的话,我会给这些答案更多的投票!

16 个解决方案

#1


36  

Without putting my brain fully into gear, from a quick scan of what you say it looks as though you need to intern() your Strings:

没有完全放入我的大脑,从快速扫描你所说的内容看起来好像你需要实习()你的字符串:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

Two Strings with the same value are otherwise not necessarily the same object.

具有相同值的两个字符串不一定是相同的对象。

Note that this may introduce a new point of contention, since deep in the VM, intern() may have to acquire a lock. I have no idea what modern VMs look like in this area, but one hopes they are fiendishly optimised.

请注意,这可能会引入新的争用点,因为在VM的深处,intern()可能必须获取锁。我不知道现代虚拟机在这个领域是什么样的,但人们希望它们能够进行极端优化。

I assume you know that StaticCache still needs to be thread-safe. But the contention there should be tiny compared with what you'd have if you were locking on the cache rather than just the key while calling getSomeDataForEmail.

我假设您知道StaticCache仍然需要是线程安全的。但是,如果你在调用getSomeDataForEmail时锁定缓存而不仅仅是密钥,那么与那里的争论相比应该是微不足道的。

Response to question update:

回答问题更新:

I think that's because a string literal always yields the same object. Dave Costa points out in a comment that it's even better than that: a literal always yields the canonical representation. So all String literals with the same value anywhere in the program would yield the same object.

我认为这是因为字符串文字总是产生相同的对象。戴夫·科斯塔在评论中指出,它甚至比这更好:文字总是产生规范表示。因此,程序中任何位置具有相同值的所有String文字都将产生相同的对象。

Edit

Others have pointed out that synchronizing on intern strings is actually a really bad idea - partly because creating intern strings is permitted to cause them to exist in perpetuity, and partly because if more than one bit of code anywhere in your program synchronizes on intern strings, you have dependencies between those bits of code, and preventing deadlocks or other bugs may be impossible.

其他人指出,实习生字符串的同步实际上是一个非常糟糕的主意 - 部分原因是允许创建实习字符串使其永久存在,部分原因是如果程序中任何地方的代码不止一位在实习字符串上同步,你有这些代码之间的依赖关系,防止死锁或其他错误可能是不可能的。

Strategies to avoid this by storing a lock object per key string are being developed in other answers as I type.

在我输入的其他答案中,正在开发通过为每个键字符串存储锁定对象来避免这种情况的策略。

Here's an alternative - it still uses a singular lock, but we know we're going to need one of those for the cache anyway, and you were talking about 50 threads, not 5000, so that may not be fatal. I'm also assuming that the performance bottleneck here is slow blocking I/O in DoSlowThing() which will therefore hugely benefit from not being serialised. If that's not the bottleneck, then:

这里有一个替代方案 - 它仍然使用单一锁,但我们知道无论如何我们将需要其中一个用于缓存,而你谈论的是50个线程,而不是5000个,所以这可能不是致命的。我还假设这里的性能瓶颈是在DoSlowThing()中阻塞I / O很慢,因此不会被序列化带来巨大好处。如果这不是瓶颈,那么:

  • If the CPU is busy then this approach may not be sufficient and you need another approach.
  • 如果CPU忙,那么这种方法可能还不够,您需要另一种方法。

  • If the CPU is not busy, and access to server is not a bottleneck, then this approach is overkill, and you might as well forget both this and per-key locking, put a big synchronized(StaticCache) around the whole operation, and do it the easy way.
  • 如果CPU不忙,并且访问服务器不是瓶颈,那么这种方法就是矫枉过正,你不妨忘记这个和每个键的锁定,在整个操作周围放一个大的同步(StaticCache),并且做这很简单。

Obviously this approach needs to be soak tested for scalability before use -- I guarantee nothing.

显然,这种方法需要在使用前进行可靠性测试 - 我保证不会。

This code does NOT require that StaticCache is synchronized or otherwise thread-safe. That needs to be revisited if any other code (for example scheduled clean-up of old data) ever touches the cache.

此代码不要求StaticCache同步或以其他方式线程安全。如果任何其他代码(例如计划的旧数据清理)触及缓存,则需要重新访问。

IN_PROGRESS is a dummy value - not exactly clean, but the code's simple and it saves having two hashtables. It doesn't handle InterruptedException because I don't know what your app wants to do in that case. Also, if DoSlowThing() consistently fails for a given key this code as it stands is not exactly elegant, since every thread through will retry it. Since I don't know what the failure criteria are, and whether they are liable to be temporary or permanent, I don't handle this either, I just make sure threads don't block forever. In practice you may want to put a data value in the cache which indicates 'not available', perhaps with a reason, and a timeout for when to retry.

IN_PROGRESS是一个虚拟值 - 不完全干净,但代码很简单,它节省了两个哈希表。它不处理InterruptedException,因为在这种情况下我不知道你的应用程序想要做什么。此外,如果DoSlowThing()对于给定键始终失败,则此代码不是很完美,因为每个线程都会重试它。由于我不知道失败的标准是什么,以及它们是否可能是临时的或永久性的,我也不会处理这个问题,我只是确保线程不会永远阻塞。实际上,您可能希望在缓存中放置一个数据值,表示“不可用”,可能有原因,以及何时重试超时。

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

Every time anything is added to the cache, all threads wake up and check the cache (no matter what key they're after), so it's possible to get better performance with less contentious algorithms. However, much of that work will take place during your copious idle CPU time blocking on I/O, so it may not be a problem.

每次将任何内容添加到缓存中时,所有线程都会唤醒并检查缓存(无论它们处于什么密钥之后),因此可以通过较少争用的算法获得更好的性能。但是,大部分工作将在I / O上大量空闲CPU时间阻塞期间进行,因此可能不是问题。

This code could be commoned-up for use with multiple caches, if you define suitable abstractions for the cache and its associated lock, the data it returns, the IN_PROGRESS dummy, and the slow operation to perform. Rolling the whole thing into a method on the cache might not be a bad idea.

如果为缓存及其关联的锁定定义合适的抽象,返回的数据,IN_PROGRESS虚拟以及执行速度慢的操作,则可以将此代码用于多个缓存。将整个事物滚动到缓存上的方法可能不是一个坏主意。

#2


24  

Synchronizing on an intern'd String might not be a good idea at all - by interning it, the String turns into a global object, and if you synchronize on the same interned strings in different parts of your application, you might get really weird and basically undebuggable synchronization issues such as deadlocks. It might seem unlikely, but when it happens you are really screwed. As a general rule, only ever synchronize on a local object where you're absolutely sure that no code outside of your module might lock it.

在intern'd String上进行同步可能根本不是一个好主意 - 通过实习,String变成一个全局对象,如果你在应用程序的不同部分同步interned字符串,你可能会变得非常奇怪,基本上是不可解决的同步问题,例如死锁。这似乎不太可能,但当它发生时,你真的搞砸了。作为一般规则,只能在本地对象上进行同步,您绝对确定模块外部的代码不会锁定它。

In your case, you can use a synchronized hashtable to store locking objects for your keys.

在您的情况下,您可以使用同步哈希表来存储密钥的锁定对象。

E.g.:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

This code has a race condition, where two threads might put an object into the lock table after each other. This should however not be a problem, because then you only have one more thread calling the webservice and updating the cache, which shouldn't be a problem.

此代码具有竞争条件,其中两个线程可能会将对象彼此放入锁定表中。这应该不是问题,因为那时你只有一个线程调用webservice并更新缓存,这应该不是问题。

If you're invalidating the cache after some time, you should check whether data is null again after retrieving it from the cache, in the lock != null case.

如果在一段时间后使缓存失效,则应在从锁定!= null情况下从缓存中检索数据后再检查数据是否为空。

Alternatively, and much easier, you can make the whole cache lookup method ("getSomeDataByEmail") synchronized. This will mean that all threads have to synchronize when they access the cache, which might be a performance problem. But as always, try this simple solution first and see if it's really a problem! In many cases it should not be, as you probably spend much more time processing the result than synchronizing.

或者,更容易,您可以使整个缓存查找方法(“getSomeDataByEmail”)同步。这意味着所有线程在访问缓存时都必须进行同步,这可能是性能问题。但一如既往,首先尝试这个简单的解决方案,看看它是否真的是一个问题!在许多情况下它不应该是,因为您可能花费更多时间处理结果而不是同步。

#3


9  

Strings are not good candidates for synchronization. If you must synchronize on a String ID, it can be done by using the string to create a mutex (see "synchronizing on an ID"). Whether the cost of that algorithm is worth it depends on whether invoking your service involves any significant I/O.

字符串不适合同步。如果必须同步字符串ID,可以使用字符串创建互斥锁(请参阅“同步ID”)。该算法的成本是否值得,取决于调用您的服务是否涉及任何重要的I / O.

Also:

  • I hope the StaticCache.get() and set() methods are threadsafe.
  • 我希望StaticCache.get()和set()方法是线程安全的。

  • String.intern() comes at a cost (one that varies between VM implementations) and should be used with care.
  • String.intern()需要付出代价(在VM实现之间有所不同),应谨慎使用。

#4


5  

Others have suggested interning the strings, and that will work.

其他人建议实习字符串,这将有效。

The problem is that Java has to keep interned strings around. I was told it does this even if you're not holding a reference because the value needs to be the same the next time someone uses that string. This means interning all the strings may start eating up memory, which with the load you're describing could be a big problem.

问题是Java必须保持内部字符串。我被告知即使你没有持有引用也会这样做,因为下次有人使用该字符串时,该值必须相同。这意味着实习所有字符串可能会开始占用内存,而你所描述的负载可能是一个大问题。

I have seen two solutions to this:

我已经看到了两个解决方案:

You could synchronize on another object

您可以在另一个对象上进行同步

Instead of the email, make an object that holds the email (say the User object) that holds the value of email as a variable. If you already have another object that represents the person (say you already pulled something from the DB based on their email) you could use that. By implementing the equals method and the hashcode method you can make sure Java considers the objects the same when you do a static cache.contains() to find out if the data is already in the cache (you'll have to synchronize on the cache).

而不是使用电子邮件,创建一个包含电子邮件(例如User对象)的对象,该电子邮件将电子邮件的值保存为变量。如果您已经有另一个代表该人的对象(比如您已经根据他们的电子邮件从数据库中提取了某些内容),那么您可以使用它。通过实现equals方法和hashcode方法,您可以确保Java在执行静态cache.contains()时认为对象是相同的,以确定数据是否已经在缓存中(您必须在缓存上进行同步) )。

Actually, you could keep a second Map for objects to lock on. Something like this:

实际上,您可以为要锁定的对象保留第二个Map。像这样的东西:

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

This will prevent 15 fetches on the same email address at one. You'll need something to prevent too many entries from ending up in the emailLocks map. Using LRUMaps from Apache Commons would do it.

这样可以防止在同一个电子邮件地址上进行15次提取。您需要一些东西来阻止太多条目在emailLocks映射中结束。使用Apache Commons的LRUMaps可以做到这一点。

This will need some tweaking, but it may solve your problem.

这需要一些调整,但它可以解决您的问题。

Use a different key

使用其他密钥

If you are willing to put up with possible errors (I don't know how important this is) you could use the hashcode of the String as the key. ints don't need to be interned.

如果您愿意忍受可能的错误(我不知道这有多重要),您可以使用String的哈希码作为密钥。整理不需要实习。

Summary

I hope this helps. Threading is fun, isn't it? You could also use the session to set a value meaning "I'm already working on finding this" and check that to see if the second (third, Nth) thread needs to attempt to create the or just wait for the result to show up in the cache. I guess I had three suggestions.

我希望这有帮助。线程很有趣,不是吗?您还可以使用会话设置一个值,意思是“我已经在努力找到这个”并检查是否需要尝试创建第二个(第三个,第N个)线程或者只是等待结果显示在缓存中。我想我有三个建议。

#5


5  

You can use the 1.5 concurrency utilities to provide a cache designed to allow multiple concurrent access, and a single point of addition (i.e. only one thread ever performing the expensive object "creation"):

您可以使用1.5并发实用程序来提供旨在允许多个并发访问的缓存,以及单个添加点(即,只有一个线程执行昂贵的对象“创建”):

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

Obviously, this doesn't handle exceptions as you'd want to, and the cache doesn't have eviction built in. Perhaps you could use it as a basis to change your StaticCache class, though.

显然,这并不像你想要的那样处理异常,并且缓存没有内置的驱逐。也许你可以用它作为改变你的StaticCache类的基础。

#6


2  

Your main problem is not just that there might be multiple instances of String with the same value. The main problem is that you need to have only one monitor on which to synchronize for accessing the StaticCache object. Otherwise multiple threads might end up concurrently modifying StaticCache (albeit under different keys), which most likely doesn't support concurrent modification.

您的主要问题不仅仅是可能存在具有相同值的多个String实例。主要问题是您只需要一个用于同步的监视器来访问StaticCache对象。否则,多个线程可能最终同时修改StaticCache(尽管在不同的密钥下),这很可能不支持并发修改。

#7


2  

The call:

   final String key = "Data-" + email;

creates a new object every time the method is called. Because that object is what you use to lock, and every call to this method creates a new object, then you are not really synchronizing access to the map based on the key.

每次调用方法时都会创建一个新对象。因为该对象是您用来锁定的对象,并且每次调用此方法都会创建一个新对象,所以您实际上并不是基于该键同步对地图的访问。

This further explain your edit. When you have a static string, then it will work.

这进一步解释了您的编辑。如果你有一个静态字符串,那么它将起作用。

Using intern() solves the problem, because it returns the string from an internal pool kept by the String class, that ensures that if two strings are equal, the one in the pool will be used. See

使用intern()解决了这个问题,因为它从String类保存的内部池中返回字符串,这确保了如果两个字符串相等,则将使用池中的字符串。看到

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()

#8


2  

Use a decent caching framework such as ehcache.

使用像ehcache这样的体面缓存框架。

Implementing a good cache is not as easy as some people believe.

实现一个好的缓存并不像有些人认为的那么容易。

Regarding the comment that String.intern() is a source of memory leaks, that is actually not true. Interned Strings are garbage collected,it just might take longer because on certain JVM'S (SUN) they are stored in Perm space which is only touched by full GC's.

关于String.intern()是内存泄漏源的注释,实际上并非如此。 Interned Strings是垃圾收集的,它可能需要更长的时间,因为在某些JVM'S(SUN)上,它们存储在Perm空间中,只有完整的GC触及它。

#9


1  

This is rather late, but there is quite a lot of incorrect code presented here.

这是相当晚的,但这里提供了很多不正确的代码。

In this example:

在这个例子中:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

The synchronization is incorrectly scoped. For a static cache that supports a get/put API, there should be at least synchronization around the get and getIfAbsentPut type operations, for safe access to the cache. The scope of synchronization will be the cache itself.

同步的范围不正确。对于支持get / put API的静态缓存,至少应该围绕get和getIfAbsentPut类型操作进行同步,以便安全访问缓存。同步范围将是缓存本身。

If updates must be made to the data elements themselves, that adds an additional layer of synchronization, which should be on the individual data elements.

如果必须对数据元素本身进行更新,则会增加一个额外的同步层,该层应该位于各个数据元素上。

SynchronizedMap can be used in place of explicit synchronization, but care must still be observed. If the wrong APIs are used (get and put instead of putIfAbsent) then the operations won't have the necessary synchronization, despite the use of the synchronized map. Notice the complications introduced by the use of putIfAbsent: Either, the put value must be computed even in cases when it is not needed (because the put cannot know if the put value is needed until the cache contents are examined), or requires a careful use of delegation (say, using Future, which works, but is somewhat of a mismatch; see below), where the put value is obtained on demand if needed.

可以使用SynchronizedMap代替显式同步,但仍必须注意。如果使用了错误的API(get和put而不是putIfAbsent),则尽管使用了synchronized映射,但操作将没有必要的同步。注意使用putIfAbsent引入的复杂性:要么,即使在不需要的情况下也必须计算put值(因为put不知道在检查缓存内容之前是否需要put值),或者需要小心使用委托(例如,使用Future,它有效,但有些不匹配;见下文),其中put值是根据需要获得的。

The use of Futures is possible, but seems rather awkward, and perhaps a bit of overengineering. The Future API is at it's core for asynchronous operations, in particular, for operations which may not complete immediately. Involving Future very probably adds a layer of thread creation -- extra probably unnecessary complications.

期货的使用是可能的,但似乎相当尴尬,也许有点过度工程。 Future API是异步操作的核心,特别是对于可能无法立即完成的操作。涉及Future很可能会增加一层线程创建 - 额外可能是不必要的复杂问题。

The main problem of using Future for this type of operation is that Future inherently ties in multi-threading. Use of Future when a new thread is not necessary means ignoring a lot of the machinery of Future, making it an overly heavy API for this use.

使用Future进行此类操作的主要问题是Future在多线程中具有内在联系。在没有必要使用新线程时使用Future意味着忽略了Future的许多机制,使其成为过度使用的API。

#10


1  

Here is a safe short Java 8 solution that uses a map of dedicated lock objects for synchronization:

这是一个安全的短Java 8解决方案,它使用专用锁对象的映射进行同步:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

It has a drawback that keys and lock objects would retain in map forever.

它有一个缺点,键和锁定对象将永远保留在映射中。

This can be worked around like this:

这可以解决这个问题:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key);
        }
    }
    return data;
}

But then popular keys would be constantly reinserted in map with lock objects being reallocated.

但随后流行的密钥将不断重新插入地图中,并重新分配锁定对象。

Update: And this leaves race condition possibility when two threads would concurrently enter synchronized section for the same key but with different locks.

更新:当两个线程同时进入相同键但具有不同锁的同步部分时,这会留下竞争条件。

So it may be more safe and efficient to use expiring Guava Cache:

因此,使用过期的Guava缓存可能更安全有效:

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Note that it's assumed here that StaticCache is thread-safe and wouldn't suffer from concurrent reads and writes for different keys.

请注意,这里假设StaticCache是​​线程安全的,不会受到不同键的并发读写操作的影响。

#11


0  

Why not just render a static html page that gets served to the user and regenerated every x minutes?

为什么不渲染一个静态的html页面,该页面被提供给用户并每隔x分钟重新生成一次?

#12


0  

I'd also suggest getting rid of the string concatenation entirely if you don't need it.

如果你不需要,我还建议完全摆脱字符串连接。

final String key = "Data-" + email;

Is there other things/types of objects in the cache that use the email address that you need that extra "Data-" at the beginning of the key?

缓存中是否有其他事物/类型的对象使用您需要在密钥开头添加额外“数据”的电子邮件地址?

if not, i'd just make that

如果没有,我就是这么做的

final String key = email;

and you avoid all that extra string creation too.

并且你也避免了所有额外的字符串创建。

#13


0  

other way synchronizing on string object :

其他方式同步字符串对象:

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if(obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if(obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}

#14


0  

In case others have a similar problem, the following code works, as far as I can tell:

如果其他人有类似的问题,以下代码可以工作,据我所知:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

In the case of the OP, it would be used like this:

在OP的情况下,它将像这样使用:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

If nothing should be returned from the synchronized code, the synchronize method can be written like this:

如果不应从同步代码返回任何内容,则可以像这样编写同步方法:

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}

#15


0  

This question seems to me a bit too broad, and therefore it instigated equally broad set of answers. So I'll try to answer the question I have been redirected from, unfortunately that one has been closed as duplicate.

这个问题在我看来有点过于宽泛,因此它煽动了同样广泛的答案。所以我会尝试回答我被重定向的问题,不幸的是,一个已被关闭为重复。

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

Upon the (outer) lock operation the (inner) lock is acquired to get an exclusive access to the map for a short time, and if the correspondent object is already in the map, the current thread will wait, otherwise it will put new Condition to the map, release the (inner) lock and proceed, and the (outer) lock is considered obtained. The (outer) unlock operation, first acquiring an (inner) lock, will signal on Condition and then remove the object from the map.

在(外部)锁定操作时,获取(内部)锁定以获得对地图的独占访问短时间,并且如果对应的对象已经在地图中,则当前线程将等待,否则它将放置新的条件到地图,释放(内部)锁并继续,并认为获得(外部)锁。 (外部)解锁操作,首先获取(内部)锁定,将在条件上发出信号,然后从地图中移除对象。

The class does not use concurrent version of Map, because every access to it is guarded by single (inner) lock.

该类不使用Map的并发版本,因为对它的每次访问都由单(内)锁保护。

Please notice, the semantic of lock() method of this class is different that of ReentrantLock.lock(), the repeated lock() invocations without paired unlock() will hang current thread indefinitely.

请注意,此类的lock()方法的语义与ReentrantLock.lock()的语义不同,重复的lock()调用没有配对unlock()将无限期地挂起当前线程。

An example of usage that might be applicable to the situation, the OP described

OP描述的可能适用于该情况的使用示例

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }

#16


0  

I've added a small lock class that can lock/synchronize on any key, including strings.

我添加了一个小锁类,可以锁定/同步任何键,包括字符串。

See implementation for Java 8, Java 6 and a small test.

请参阅Java 8,Java 6的实现和一个小测试。

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

public class DynamicKeyLock implements Lock { private final static ConcurrentHashMap locksMap = new ConcurrentHashMap(); private final T key;

公共类DynamicKeyLock实现Lock {private final static ConcurrentHashMap locksMap = new ConcurrentHashMap();私人决赛T键;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Test:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}

#1


36  

Without putting my brain fully into gear, from a quick scan of what you say it looks as though you need to intern() your Strings:

没有完全放入我的大脑,从快速扫描你所说的内容看起来好像你需要实习()你的字符串:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

Two Strings with the same value are otherwise not necessarily the same object.

具有相同值的两个字符串不一定是相同的对象。

Note that this may introduce a new point of contention, since deep in the VM, intern() may have to acquire a lock. I have no idea what modern VMs look like in this area, but one hopes they are fiendishly optimised.

请注意,这可能会引入新的争用点,因为在VM的深处,intern()可能必须获取锁。我不知道现代虚拟机在这个领域是什么样的,但人们希望它们能够进行极端优化。

I assume you know that StaticCache still needs to be thread-safe. But the contention there should be tiny compared with what you'd have if you were locking on the cache rather than just the key while calling getSomeDataForEmail.

我假设您知道StaticCache仍然需要是线程安全的。但是,如果你在调用getSomeDataForEmail时锁定缓存而不仅仅是密钥,那么与那里的争论相比应该是微不足道的。

Response to question update:

回答问题更新:

I think that's because a string literal always yields the same object. Dave Costa points out in a comment that it's even better than that: a literal always yields the canonical representation. So all String literals with the same value anywhere in the program would yield the same object.

我认为这是因为字符串文字总是产生相同的对象。戴夫·科斯塔在评论中指出,它甚至比这更好:文字总是产生规范表示。因此,程序中任何位置具有相同值的所有String文字都将产生相同的对象。

Edit

Others have pointed out that synchronizing on intern strings is actually a really bad idea - partly because creating intern strings is permitted to cause them to exist in perpetuity, and partly because if more than one bit of code anywhere in your program synchronizes on intern strings, you have dependencies between those bits of code, and preventing deadlocks or other bugs may be impossible.

其他人指出,实习生字符串的同步实际上是一个非常糟糕的主意 - 部分原因是允许创建实习字符串使其永久存在,部分原因是如果程序中任何地方的代码不止一位在实习字符串上同步,你有这些代码之间的依赖关系,防止死锁或其他错误可能是不可能的。

Strategies to avoid this by storing a lock object per key string are being developed in other answers as I type.

在我输入的其他答案中,正在开发通过为每个键字符串存储锁定对象来避免这种情况的策略。

Here's an alternative - it still uses a singular lock, but we know we're going to need one of those for the cache anyway, and you were talking about 50 threads, not 5000, so that may not be fatal. I'm also assuming that the performance bottleneck here is slow blocking I/O in DoSlowThing() which will therefore hugely benefit from not being serialised. If that's not the bottleneck, then:

这里有一个替代方案 - 它仍然使用单一锁,但我们知道无论如何我们将需要其中一个用于缓存,而你谈论的是50个线程,而不是5000个,所以这可能不是致命的。我还假设这里的性能瓶颈是在DoSlowThing()中阻塞I / O很慢,因此不会被序列化带来巨大好处。如果这不是瓶颈,那么:

  • If the CPU is busy then this approach may not be sufficient and you need another approach.
  • 如果CPU忙,那么这种方法可能还不够,您需要另一种方法。

  • If the CPU is not busy, and access to server is not a bottleneck, then this approach is overkill, and you might as well forget both this and per-key locking, put a big synchronized(StaticCache) around the whole operation, and do it the easy way.
  • 如果CPU不忙,并且访问服务器不是瓶颈,那么这种方法就是矫枉过正,你不妨忘记这个和每个键的锁定,在整个操作周围放一个大的同步(StaticCache),并且做这很简单。

Obviously this approach needs to be soak tested for scalability before use -- I guarantee nothing.

显然,这种方法需要在使用前进行可靠性测试 - 我保证不会。

This code does NOT require that StaticCache is synchronized or otherwise thread-safe. That needs to be revisited if any other code (for example scheduled clean-up of old data) ever touches the cache.

此代码不要求StaticCache同步或以其他方式线程安全。如果任何其他代码(例如计划的旧数据清理)触及缓存,则需要重新访问。

IN_PROGRESS is a dummy value - not exactly clean, but the code's simple and it saves having two hashtables. It doesn't handle InterruptedException because I don't know what your app wants to do in that case. Also, if DoSlowThing() consistently fails for a given key this code as it stands is not exactly elegant, since every thread through will retry it. Since I don't know what the failure criteria are, and whether they are liable to be temporary or permanent, I don't handle this either, I just make sure threads don't block forever. In practice you may want to put a data value in the cache which indicates 'not available', perhaps with a reason, and a timeout for when to retry.

IN_PROGRESS是一个虚拟值 - 不完全干净,但代码很简单,它节省了两个哈希表。它不处理InterruptedException,因为在这种情况下我不知道你的应用程序想要做什么。此外,如果DoSlowThing()对于给定键始终失败,则此代码不是很完美,因为每个线程都会重试它。由于我不知道失败的标准是什么,以及它们是否可能是临时的或永久性的,我也不会处理这个问题,我只是确保线程不会永远阻塞。实际上,您可能希望在缓存中放置一个数据值,表示“不可用”,可能有原因,以及何时重试超时。

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

Every time anything is added to the cache, all threads wake up and check the cache (no matter what key they're after), so it's possible to get better performance with less contentious algorithms. However, much of that work will take place during your copious idle CPU time blocking on I/O, so it may not be a problem.

每次将任何内容添加到缓存中时,所有线程都会唤醒并检查缓存(无论它们处于什么密钥之后),因此可以通过较少争用的算法获得更好的性能。但是,大部分工作将在I / O上大量空闲CPU时间阻塞期间进行,因此可能不是问题。

This code could be commoned-up for use with multiple caches, if you define suitable abstractions for the cache and its associated lock, the data it returns, the IN_PROGRESS dummy, and the slow operation to perform. Rolling the whole thing into a method on the cache might not be a bad idea.

如果为缓存及其关联的锁定定义合适的抽象,返回的数据,IN_PROGRESS虚拟以及执行速度慢的操作,则可以将此代码用于多个缓存。将整个事物滚动到缓存上的方法可能不是一个坏主意。

#2


24  

Synchronizing on an intern'd String might not be a good idea at all - by interning it, the String turns into a global object, and if you synchronize on the same interned strings in different parts of your application, you might get really weird and basically undebuggable synchronization issues such as deadlocks. It might seem unlikely, but when it happens you are really screwed. As a general rule, only ever synchronize on a local object where you're absolutely sure that no code outside of your module might lock it.

在intern'd String上进行同步可能根本不是一个好主意 - 通过实习,String变成一个全局对象,如果你在应用程序的不同部分同步interned字符串,你可能会变得非常奇怪,基本上是不可解决的同步问题,例如死锁。这似乎不太可能,但当它发生时,你真的搞砸了。作为一般规则,只能在本地对象上进行同步,您绝对确定模块外部的代码不会锁定它。

In your case, you can use a synchronized hashtable to store locking objects for your keys.

在您的情况下,您可以使用同步哈希表来存储密钥的锁定对象。

E.g.:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

This code has a race condition, where two threads might put an object into the lock table after each other. This should however not be a problem, because then you only have one more thread calling the webservice and updating the cache, which shouldn't be a problem.

此代码具有竞争条件,其中两个线程可能会将对象彼此放入锁定表中。这应该不是问题,因为那时你只有一个线程调用webservice并更新缓存,这应该不是问题。

If you're invalidating the cache after some time, you should check whether data is null again after retrieving it from the cache, in the lock != null case.

如果在一段时间后使缓存失效,则应在从锁定!= null情况下从缓存中检索数据后再检查数据是否为空。

Alternatively, and much easier, you can make the whole cache lookup method ("getSomeDataByEmail") synchronized. This will mean that all threads have to synchronize when they access the cache, which might be a performance problem. But as always, try this simple solution first and see if it's really a problem! In many cases it should not be, as you probably spend much more time processing the result than synchronizing.

或者,更容易,您可以使整个缓存查找方法(“getSomeDataByEmail”)同步。这意味着所有线程在访问缓存时都必须进行同步,这可能是性能问题。但一如既往,首先尝试这个简单的解决方案,看看它是否真的是一个问题!在许多情况下它不应该是,因为您可能花费更多时间处理结果而不是同步。

#3


9  

Strings are not good candidates for synchronization. If you must synchronize on a String ID, it can be done by using the string to create a mutex (see "synchronizing on an ID"). Whether the cost of that algorithm is worth it depends on whether invoking your service involves any significant I/O.

字符串不适合同步。如果必须同步字符串ID,可以使用字符串创建互斥锁(请参阅“同步ID”)。该算法的成本是否值得,取决于调用您的服务是否涉及任何重要的I / O.

Also:

  • I hope the StaticCache.get() and set() methods are threadsafe.
  • 我希望StaticCache.get()和set()方法是线程安全的。

  • String.intern() comes at a cost (one that varies between VM implementations) and should be used with care.
  • String.intern()需要付出代价(在VM实现之间有所不同),应谨慎使用。

#4


5  

Others have suggested interning the strings, and that will work.

其他人建议实习字符串,这将有效。

The problem is that Java has to keep interned strings around. I was told it does this even if you're not holding a reference because the value needs to be the same the next time someone uses that string. This means interning all the strings may start eating up memory, which with the load you're describing could be a big problem.

问题是Java必须保持内部字符串。我被告知即使你没有持有引用也会这样做,因为下次有人使用该字符串时,该值必须相同。这意味着实习所有字符串可能会开始占用内存,而你所描述的负载可能是一个大问题。

I have seen two solutions to this:

我已经看到了两个解决方案:

You could synchronize on another object

您可以在另一个对象上进行同步

Instead of the email, make an object that holds the email (say the User object) that holds the value of email as a variable. If you already have another object that represents the person (say you already pulled something from the DB based on their email) you could use that. By implementing the equals method and the hashcode method you can make sure Java considers the objects the same when you do a static cache.contains() to find out if the data is already in the cache (you'll have to synchronize on the cache).

而不是使用电子邮件,创建一个包含电子邮件(例如User对象)的对象,该电子邮件将电子邮件的值保存为变量。如果您已经有另一个代表该人的对象(比如您已经根据他们的电子邮件从数据库中提取了某些内容),那么您可以使用它。通过实现equals方法和hashcode方法,您可以确保Java在执行静态cache.contains()时认为对象是相同的,以确定数据是否已经在缓存中(您必须在缓存上进行同步) )。

Actually, you could keep a second Map for objects to lock on. Something like this:

实际上,您可以为要锁定的对象保留第二个Map。像这样的东西:

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

This will prevent 15 fetches on the same email address at one. You'll need something to prevent too many entries from ending up in the emailLocks map. Using LRUMaps from Apache Commons would do it.

这样可以防止在同一个电子邮件地址上进行15次提取。您需要一些东西来阻止太多条目在emailLocks映射中结束。使用Apache Commons的LRUMaps可以做到这一点。

This will need some tweaking, but it may solve your problem.

这需要一些调整,但它可以解决您的问题。

Use a different key

使用其他密钥

If you are willing to put up with possible errors (I don't know how important this is) you could use the hashcode of the String as the key. ints don't need to be interned.

如果您愿意忍受可能的错误(我不知道这有多重要),您可以使用String的哈希码作为密钥。整理不需要实习。

Summary

I hope this helps. Threading is fun, isn't it? You could also use the session to set a value meaning "I'm already working on finding this" and check that to see if the second (third, Nth) thread needs to attempt to create the or just wait for the result to show up in the cache. I guess I had three suggestions.

我希望这有帮助。线程很有趣,不是吗?您还可以使用会话设置一个值,意思是“我已经在努力找到这个”并检查是否需要尝试创建第二个(第三个,第N个)线程或者只是等待结果显示在缓存中。我想我有三个建议。

#5


5  

You can use the 1.5 concurrency utilities to provide a cache designed to allow multiple concurrent access, and a single point of addition (i.e. only one thread ever performing the expensive object "creation"):

您可以使用1.5并发实用程序来提供旨在允许多个并发访问的缓存,以及单个添加点(即,只有一个线程执行昂贵的对象“创建”):

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

Obviously, this doesn't handle exceptions as you'd want to, and the cache doesn't have eviction built in. Perhaps you could use it as a basis to change your StaticCache class, though.

显然,这并不像你想要的那样处理异常,并且缓存没有内置的驱逐。也许你可以用它作为改变你的StaticCache类的基础。

#6


2  

Your main problem is not just that there might be multiple instances of String with the same value. The main problem is that you need to have only one monitor on which to synchronize for accessing the StaticCache object. Otherwise multiple threads might end up concurrently modifying StaticCache (albeit under different keys), which most likely doesn't support concurrent modification.

您的主要问题不仅仅是可能存在具有相同值的多个String实例。主要问题是您只需要一个用于同步的监视器来访问StaticCache对象。否则,多个线程可能最终同时修改StaticCache(尽管在不同的密钥下),这很可能不支持并发修改。

#7


2  

The call:

   final String key = "Data-" + email;

creates a new object every time the method is called. Because that object is what you use to lock, and every call to this method creates a new object, then you are not really synchronizing access to the map based on the key.

每次调用方法时都会创建一个新对象。因为该对象是您用来锁定的对象,并且每次调用此方法都会创建一个新对象,所以您实际上并不是基于该键同步对地图的访问。

This further explain your edit. When you have a static string, then it will work.

这进一步解释了您的编辑。如果你有一个静态字符串,那么它将起作用。

Using intern() solves the problem, because it returns the string from an internal pool kept by the String class, that ensures that if two strings are equal, the one in the pool will be used. See

使用intern()解决了这个问题,因为它从String类保存的内部池中返回字符串,这确保了如果两个字符串相等,则将使用池中的字符串。看到

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()

#8


2  

Use a decent caching framework such as ehcache.

使用像ehcache这样的体面缓存框架。

Implementing a good cache is not as easy as some people believe.

实现一个好的缓存并不像有些人认为的那么容易。

Regarding the comment that String.intern() is a source of memory leaks, that is actually not true. Interned Strings are garbage collected,it just might take longer because on certain JVM'S (SUN) they are stored in Perm space which is only touched by full GC's.

关于String.intern()是内存泄漏源的注释,实际上并非如此。 Interned Strings是垃圾收集的,它可能需要更长的时间,因为在某些JVM'S(SUN)上,它们存储在Perm空间中,只有完整的GC触及它。

#9


1  

This is rather late, but there is quite a lot of incorrect code presented here.

这是相当晚的,但这里提供了很多不正确的代码。

In this example:

在这个例子中:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

The synchronization is incorrectly scoped. For a static cache that supports a get/put API, there should be at least synchronization around the get and getIfAbsentPut type operations, for safe access to the cache. The scope of synchronization will be the cache itself.

同步的范围不正确。对于支持get / put API的静态缓存,至少应该围绕get和getIfAbsentPut类型操作进行同步,以便安全访问缓存。同步范围将是缓存本身。

If updates must be made to the data elements themselves, that adds an additional layer of synchronization, which should be on the individual data elements.

如果必须对数据元素本身进行更新,则会增加一个额外的同步层,该层应该位于各个数据元素上。

SynchronizedMap can be used in place of explicit synchronization, but care must still be observed. If the wrong APIs are used (get and put instead of putIfAbsent) then the operations won't have the necessary synchronization, despite the use of the synchronized map. Notice the complications introduced by the use of putIfAbsent: Either, the put value must be computed even in cases when it is not needed (because the put cannot know if the put value is needed until the cache contents are examined), or requires a careful use of delegation (say, using Future, which works, but is somewhat of a mismatch; see below), where the put value is obtained on demand if needed.

可以使用SynchronizedMap代替显式同步,但仍必须注意。如果使用了错误的API(get和put而不是putIfAbsent),则尽管使用了synchronized映射,但操作将没有必要的同步。注意使用putIfAbsent引入的复杂性:要么,即使在不需要的情况下也必须计算put值(因为put不知道在检查缓存内容之前是否需要put值),或者需要小心使用委托(例如,使用Future,它有效,但有些不匹配;见下文),其中put值是根据需要获得的。

The use of Futures is possible, but seems rather awkward, and perhaps a bit of overengineering. The Future API is at it's core for asynchronous operations, in particular, for operations which may not complete immediately. Involving Future very probably adds a layer of thread creation -- extra probably unnecessary complications.

期货的使用是可能的,但似乎相当尴尬,也许有点过度工程。 Future API是异步操作的核心,特别是对于可能无法立即完成的操作。涉及Future很可能会增加一层线程创建 - 额外可能是不必要的复杂问题。

The main problem of using Future for this type of operation is that Future inherently ties in multi-threading. Use of Future when a new thread is not necessary means ignoring a lot of the machinery of Future, making it an overly heavy API for this use.

使用Future进行此类操作的主要问题是Future在多线程中具有内在联系。在没有必要使用新线程时使用Future意味着忽略了Future的许多机制,使其成为过度使用的API。

#10


1  

Here is a safe short Java 8 solution that uses a map of dedicated lock objects for synchronization:

这是一个安全的短Java 8解决方案,它使用专用锁对象的映射进行同步:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

It has a drawback that keys and lock objects would retain in map forever.

它有一个缺点,键和锁定对象将永远保留在映射中。

This can be worked around like this:

这可以解决这个问题:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key);
        }
    }
    return data;
}

But then popular keys would be constantly reinserted in map with lock objects being reallocated.

但随后流行的密钥将不断重新插入地图中,并重新分配锁定对象。

Update: And this leaves race condition possibility when two threads would concurrently enter synchronized section for the same key but with different locks.

更新:当两个线程同时进入相同键但具有不同锁的同步部分时,这会留下竞争条件。

So it may be more safe and efficient to use expiring Guava Cache:

因此,使用过期的Guava缓存可能更安全有效:

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Note that it's assumed here that StaticCache is thread-safe and wouldn't suffer from concurrent reads and writes for different keys.

请注意,这里假设StaticCache是​​线程安全的,不会受到不同键的并发读写操作的影响。

#11


0  

Why not just render a static html page that gets served to the user and regenerated every x minutes?

为什么不渲染一个静态的html页面,该页面被提供给用户并每隔x分钟重新生成一次?

#12


0  

I'd also suggest getting rid of the string concatenation entirely if you don't need it.

如果你不需要,我还建议完全摆脱字符串连接。

final String key = "Data-" + email;

Is there other things/types of objects in the cache that use the email address that you need that extra "Data-" at the beginning of the key?

缓存中是否有其他事物/类型的对象使用您需要在密钥开头添加额外“数据”的电子邮件地址?

if not, i'd just make that

如果没有,我就是这么做的

final String key = email;

and you avoid all that extra string creation too.

并且你也避免了所有额外的字符串创建。

#13


0  

other way synchronizing on string object :

其他方式同步字符串对象:

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if(obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if(obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}

#14


0  

In case others have a similar problem, the following code works, as far as I can tell:

如果其他人有类似的问题,以下代码可以工作,据我所知:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

In the case of the OP, it would be used like this:

在OP的情况下,它将像这样使用:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

If nothing should be returned from the synchronized code, the synchronize method can be written like this:

如果不应从同步代码返回任何内容,则可以像这样编写同步方法:

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}

#15


0  

This question seems to me a bit too broad, and therefore it instigated equally broad set of answers. So I'll try to answer the question I have been redirected from, unfortunately that one has been closed as duplicate.

这个问题在我看来有点过于宽泛,因此它煽动了同样广泛的答案。所以我会尝试回答我被重定向的问题,不幸的是,一个已被关闭为重复。

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

Upon the (outer) lock operation the (inner) lock is acquired to get an exclusive access to the map for a short time, and if the correspondent object is already in the map, the current thread will wait, otherwise it will put new Condition to the map, release the (inner) lock and proceed, and the (outer) lock is considered obtained. The (outer) unlock operation, first acquiring an (inner) lock, will signal on Condition and then remove the object from the map.

在(外部)锁定操作时,获取(内部)锁定以获得对地图的独占访问短时间,并且如果对应的对象已经在地图中,则当前线程将等待,否则它将放置新的条件到地图,释放(内部)锁并继续,并认为获得(外部)锁。 (外部)解锁操作,首先获取(内部)锁定,将在条件上发出信号,然后从地图中移除对象。

The class does not use concurrent version of Map, because every access to it is guarded by single (inner) lock.

该类不使用Map的并发版本,因为对它的每次访问都由单(内)锁保护。

Please notice, the semantic of lock() method of this class is different that of ReentrantLock.lock(), the repeated lock() invocations without paired unlock() will hang current thread indefinitely.

请注意,此类的lock()方法的语义与ReentrantLock.lock()的语义不同,重复的lock()调用没有配对unlock()将无限期地挂起当前线程。

An example of usage that might be applicable to the situation, the OP described

OP描述的可能适用于该情况的使用示例

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }

#16


0  

I've added a small lock class that can lock/synchronize on any key, including strings.

我添加了一个小锁类,可以锁定/同步任何键,包括字符串。

See implementation for Java 8, Java 6 and a small test.

请参阅Java 8,Java 6的实现和一个小测试。

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

public class DynamicKeyLock implements Lock { private final static ConcurrentHashMap locksMap = new ConcurrentHashMap(); private final T key;

公共类DynamicKeyLock实现Lock {private final static ConcurrentHashMap locksMap = new ConcurrentHashMap();私人决赛T键;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Test:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}