54

我有一个 web 应用程序,我正在对它进行一些负载/性能测试,特别是在我们预计数百名用户访问同一页面并在此页面上大约每 10 秒刷新一次的功能上。我们发现我们可以使用此功能进行的一个改进领域是将来自 Web 服务的响应缓存一段时间,因为数据没有变化。

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

原始代码看起来像这样:

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;
}

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

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

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;
}

我还为“同步块之前”、“同步块内部”、“即将离开同步块”和“同步块之后”等内容添加了日志记录行,这样我就可以确定我是否有效地同步了 get/set 操作。

然而,这似乎没有奏效。我的测试日志的输出如下:

(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  

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

在 String 对象上同步是否存在问题?我认为缓存键是一个不错的选择,因为它是操作所独有的,即使final String key在方法中声明了,我认为每个线程都会获得对同一个对象的引用,因此会对此进行同步单个对象。

我在这里做错了什么?

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

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

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

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

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

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

4

20 回答 20

44

在没有让我的大脑完全投入使用的情况下,快速浏览一下您所说的内容,您似乎需要对您的字符串进行 intern():

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

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

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

我假设您知道 StaticCache 仍然需要是线程安全的。但是与调用 getSomeDataForEmail 时锁定缓存而不仅仅是键的情况相比,那里的争用应该很小。

对问题更新的回应

我认为这是因为字符串文字总是产生相同的对象。Dave Costa 在评论中指出它甚至比这更好:文字总是产生规范表示。因此,程序中任何地方具有相同值的所有字符串文字都会产生相同的对象。

编辑

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

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

这是另一种选择 - 它仍然使用单个锁,但我们知道无论如何我们都需要其中一个用于缓存,而且您说的是 50 个线程,而不是 5000 个,所以这可能不是致命的。我还假设这里的性能瓶颈是在 DoSlowThing() 中缓慢阻塞 I/O,因此将极大地受益于不被序列化。如果这不是瓶颈,那么:

  • 如果 CPU 很忙,那么这种方法可能还不够,您需要另一种方法。
  • 如果 CPU 不忙,并且访问服务器不是瓶颈,那么这种方法是矫枉过正的,你不妨忘记这个和 per-key 锁定,在整个操作周围放置一个大的同步(静态缓存),然后做这是简单的方法。

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

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

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();
        }
    }
}

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

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

于 2008-09-25T15:30:41.650 回答
28

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

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

例如:

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
}

这段代码有一个竞争条件,两个线程可能将一个对象依次放入锁表中。然而,这应该不是问题,因为那时您只有一个线程调用 Web 服务并更新缓存,这应该不是问题。

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

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

于 2008-09-25T15:53:37.210 回答
12

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

还:

  • 我希望StaticCache.get()set()方法是线程安全的。
  • String.intern()是有代价的(因 VM 实现而异),应谨慎使用。
于 2008-09-25T16:17:38.197 回答
10

这是一个安全的简短 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;
}

它的缺点是键和锁对象会永远保留在地图中。

这可以像这样解决:

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); // vulnerable to race-conditions
        }
    }
    return data;
}

但是随后流行的键将不断地重新插入映射中,并重新分配锁定对象。

更新:当两个线程同时进入同一个键但具有不同锁的同步部分时,这就留下了竞争条件的可能性。

所以使用过期的 Guava Cache可能更安全有效:

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;
}

请注意,这里假设它StaticCache是线程安全的,并且不会因不同键的并发读取和写入而受到影响。

于 2017-11-11T22:06:08.650 回答
6

其他人建议实习字符串,这会奏效。

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

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

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

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

实际上,您可以保留第二个 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.
}

这将防止在同一个电子邮件地址上同时进行 15 次提取。您需要一些东西来防止太多条目出现在 emailLocks 映射中。使用来自 Apache Commons 的LRUMap就可以了。

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

使用不同的密钥

如果您愿意忍受可能的错误(我不知道这有多重要),您可以使用字符串的哈希码作为键。int 不需要被实习。

概括

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

于 2008-09-25T16:04:40.070 回答
5

您可以使用 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
}

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

于 2008-09-27T17:26:01.847 回答
4

使用不错的缓存框架,例如ehcache

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

关于 String.intern() 是内存泄漏源的评论,这实际上是不正确的。内部字符串垃圾收集的,它可能需要更长时间,因为在某些 JVM(SUN)上,它们存储在 Perm 空间中,只有完整的 GC 才会触及。

于 2008-10-08T08:48:10.257 回答
2

来电:

   final String key = "Data-" + email;

每次调用该方法时都会创建一个新对象。因为该对象是您用来锁定的对象,并且每次调用此方法都会创建一个新对象,所以您实际上并没有根据密钥同步对映射的访问。

这进一步解释了您的编辑。当您有一个静态字符串时,它将起作用。

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

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

于 2008-09-25T15:39:07.557 回答
2

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

于 2008-09-25T15:39:28.570 回答
2

这个问题在我看来有点过于宽泛,因此它引发了同样广泛的答案。因此,我将尝试回答我被重定向的问题,不幸的是,该问题已被关闭为重复项。

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();
        }
    }

在(外部)lock操作时,获取(内部)锁以在短时间内获得对映射的独占访问权,如果对应对象已经在映射中,则当前线程将等待,否则将 newCondition放入map,释放(内部)锁并继续,并且(外部)锁被认为已获得。(外部)unlock操作,首先获取(内部)锁,将发出信号Condition,然后从地图中移除对象。

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

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

可能适用于这种情况的使用示例,OP 描述

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }
于 2018-05-11T18:31:56.307 回答
1

这有点晚了,但这里有很多不正确的代码。

在这个例子中:

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;
}

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

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

SynchronizedMap 可用于代替显式同步,但仍需注意。如果使用了错误的 API(get 和 put 而不是 putIfAbsent),那么即使使用了同步映射,操作也不会进行必要的同步。注意使用 putIfAbsent 引入的复杂性:要么,即使在不需要 put 值的情况下也必须计算它(因为在检查缓存内容之前,put 无法知道是否需要 put 值),或者需要小心使用委托(例如,使用 Future,它有效,但有点不匹配;见下文),如果需要,看跌值是按需获得的。

Futures 的使用是可能的,但看起来相当尴尬,而且可能有点过度设计。Future API 是异步操作的核心,特别是对于可能无法立即完成的操作。涉及 Future 很可能会增加一层线程创建——额外的可能是不必要的复杂性。

将 Future 用于此类操作的主要问题是 Future 本质上与多线程相关联。在不需要新线程时使用 Future 意味着忽略 Future 的许多机制,使其成为用于此用途的过于繁重的 API。

于 2013-08-22T19:13:46.057 回答
1

2019 年最新更新,

如果您正在寻找在 JAVA 中实现同步的新方法,那么这个答案适合您。

在此处输入图像描述

我发现了 Anatoliy Korovin 的这篇很棒的博客,这将帮助您深入了解同步。

如何在 Java 中通过对象的值来同步块

这帮助我希望新的开发人员也会发现这很有用。

于 2019-08-27T10:16:13.657 回答
0

为什么不只呈现一个静态 html 页面,该页面会提供给用户并每 x 分钟重新生成一次?

于 2008-09-25T15:59:34.013 回答
0

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

final String key = "Data-" + email;

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

如果没有,我就这么做

final String key = email;

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

于 2008-09-25T19:06:59.477 回答
0

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

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();
        }
    }
}

在 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;
    });
}

如果同步代码没有返回任何内容,则可以这样编写同步方法:

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);
            }
        }
    }
}
于 2017-11-26T00:16:14.823 回答
0

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

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

爪哇 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();
    }
}

爪哇 6:

公共类 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();
    }
}

测试:

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();
        }
    }
}
于 2018-05-27T07:18:08.897 回答
0

在你的情况下,你可以使用这样的东西(这不会泄漏任何内存):

private Synchronizer<String> synchronizer = new Synchronizer();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;

    return synchronizer.synchronizeOn(key, () -> {

        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;

    });
}

要使用它,您只需添加一个依赖项:

compile 'com.github.matejtymes:javafixes:1.3.0'
于 2019-06-20T23:08:02.680 回答
0

您应该非常小心地使用具有同步的短期对象。每个 Java 对象都有一个附加的监视器,默认情况下这个监视器是放气的;但是,如果 2 个线程争用获取监视器,监视器就会膨胀。如果该对象将长期存在,这不是问题。但是,如果对象的生命周期很短,那么清理这个膨胀的监视器可能会严重影响 GC 时间(因此延迟更高,吞吐量降低)。甚至可能很难发现 GC 时间,因为它并不总是被列出。

如果您确实想要同步,您可以使用 java.util.concurrent.Lock。或者使用手动制作的条带锁并将字符串的哈希用作该条带锁的索引。你保留这个带状锁,这样你就不会遇到 GC 问题。

所以是这样的:

static final Object[] locks = newLockArray();

Object lock = locks[hashToIndex(key.hashcode(),locks.length];
synchronized(lock){
       ....
}

int hashToIndex(int hash, int length) {
    if (hash == Integer.MIN_VALUE return 0;
    return abs(hash) % length;
}
于 2020-10-07T07:48:16.620 回答
-1

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

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
        }
    }
}
于 2016-10-12T02:33:27.700 回答
-1

如果您可以合理地保证字符串值在您的系统中是唯一的,那么您可以安全地使用 String.intern 进行同步。UUIDS 是解决这个问题的好方法。您可以通过缓存、映射或什至将 uuid 存储为实体对象上的字段,将 UUID 与您的实际字符串键相关联。

    @Service   
    public class MySyncService{

      public Map<String, String> lockMap=new HashMap<String, String>();

      public void syncMethod(String email) {

        String lock = lockMap.get(email);
        if(lock==null) {
            lock = UUID.randomUUID().toString();
            lockMap.put(email, lock);
        }   

        synchronized(lock.intern()) {
                //do your sync code here
        }
    }
于 2019-03-06T18:18:05.453 回答