代码之家  ›  专栏  ›  技术社区  ›  matt b

Java中字符串对象的同步

  •  40
  • matt b  · 技术社区  · 17 年前

    我有一个webapp,我正在做一些负载/性能测试,特别是在一个功能上,我们希望几百个用户访问同一个页面,并点击刷新大约每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 过期了,我认为我需要同步缓存获取/设置操作,而且使用缓存键似乎是对象同步的一个很好的候选者(这样,对email 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操作。

    但这似乎不起作用。我的测试日志输出如下:

    (日志输出为'threadname''记录器名称''消息')
    http-80-processor253 jsp.view-page-getsomedataformail:即将进入同步块
    http-80-processor253 jsp.view-page-getsomedataformail:内部同步块
    http-80-processor253 cache.staticcache-get:object at key[somedata-test@test.com]已过期
    http-80-processor253 cache.staticcache-get:key[somedata-test@test.com]返回值[null]
    http-80-processor263 jsp.view-page-getsomedataformail:即将进入同步块
    http-80-processor263 jsp.view-page-getsomedataformail:内部同步块
    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-getsomedataformail:即将进入同步块
    http-80-processor131 jsp.view-page-getsomedataformail:内部同步块
    http-80-processor131 cache.staticcache-get:object at key[somedata-test@test.com]已过期
    http-80-processor131 cache.staticcache-get:key[somedata-test@test.com]返回值[null]
    http-80-processor104 jsp.view-page-getsomedataformail:内部同步块
    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-getsomedataformail:即将进入同步块
    http-80-processor283 jsp.view-page-getsomedataformail:即将进入同步块
    http-80-processor2 jsp.view-page-getsomedataformail:即将进入同步块
    http-80-processor2 jsp.view-page-getsomedataformail:内部同步块

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

    在字符串对象上同步是否有问题?我认为缓存键是一个很好的选择,因为它是操作所特有的,即使 final String key 在方法中声明,我认为每个线程都将获得 同一对象 因此将在这个单一对象上同步。

    我在这里做错什么了?

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

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

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

    更新2 感谢大家的帮助!我接受了关于 intern() 它解决了我最初的问题——多个线程进入了我认为不应该进入的同步块,因为 钥匙 的值相同。

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

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

    16 回复  |  直到 7 年前
        1
  •  38
  •   community wiki 9 revs Steve Jessop    17 年前

    如果不让我的大脑完全进入状态,从你所说的内容的快速扫描来看,你似乎需要实习生()你的字符串:

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

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

    注意,这可能会引入一个新的争用点,因为在VM的深处,intern()可能需要获取一个锁。我不知道现代虚拟机在这方面是什么样子,但有人希望他们是疯狂优化。

    我假设您知道静态缓存仍然需要线程安全。但是,与调用getSomeDataForemail时锁定缓存而不仅仅是密钥相比,这里的争用应该很小。

    回答问题更新 :

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

    编辑

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

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

    这里有一个替代方法——它仍然使用一个单独的锁,但我们知道无论如何我们需要其中一个用于缓存,而您所说的是50个线程,而不是5000个,所以这可能不是致命的。我还假设这里的性能瓶颈是doslowthing()中的缓慢阻塞I/O,因此它将从没有序列化中受益匪浅。如果这不是瓶颈,那么:

    • 如果CPU很忙,那么这种方法可能不够,您需要另一种方法。
    • 如果CPU不忙,并且对服务器的访问不是瓶颈,那么这种方法就太过分了,您可能会忘记这一点和每键锁定,在整个操作周围放置一个大的同步(staticcache),并以简单的方式进行。

    显然,这种方法在使用前需要进行可伸缩性的浸泡测试——我什么也不保证。

    此代码不要求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();
            }
        }
    }
    

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

    如果为缓存及其关联锁、它返回的数据、进行中的虚拟对象以及要执行的缓慢操作定义适当的抽象,那么可以将此代码用于多个缓存。将整个事件滚动到缓存中的一个方法中可能不是一个坏主意。

        2
  •  25
  •   Martin Probst    17 年前

    在一个实习字符串上进行同步可能根本不是一个好主意——通过实习,字符串会变成一个全局对象,如果您在应用程序的不同部分在同一个实习字符串上进行同步,您可能会遇到非常奇怪的、基本上不可检测的同步问题,例如死锁。这似乎不太可能,但当它发生时,你真的是一团糟。作为一般规则,只有在绝对确定模块外部没有代码可能锁定本地对象的情况下才同步。

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

    例如。:

    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
    }
    

    此代码有一个争用条件,其中两个线程可以将一个对象依次放入锁表中。但是,这不应该是一个问题,因为您只有一个线程调用WebService并更新缓存,这不应该是一个问题。

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

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

        3
  •  9
  •   McDowell rahul gupta    17 年前

    字符串是 很好的同步候选。如果必须对字符串ID进行同步,则可以使用该字符串创建互斥体(请参见 synchronizing on an ID “”。该算法的成本是否值得依赖于调用服务是否涉及任何重要的I/O。

    也:

    • 我希望 staticcache.get()。 集() 方法是线程安全。
    • String.intern() 需要付出一定的代价(在不同的虚拟机实现中有所不同),并且应该小心使用。
        4
  •  5
  •   MBCook    17 年前

    其他人则建议把弦绑起来,这样就行了。

    问题是Java必须保持接口字符串。有人告诉我,即使不保存引用,它也会这样做,因为下次有人使用该字符串时,该值需要相同。这意味着把所有的字符串都放进去可能会占用内存,而你描述的负载可能是一个大问题。

    我看到了两种解决方案:

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

    代替电子邮件,创建一个保存电子邮件的对象(比如用户对象),该对象将电子邮件的值作为变量保存。如果你已经有了另一个代表这个人的对象(比如你已经根据他们的电子邮件从数据库中提取了一些东西),你可以使用它。通过执行等值方法和HASHCODE方法,您可以确保Java在静态缓存中对对象进行相同的处理。Cub()查找数据是否已经在缓存中(您必须在缓存上同步)。

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

    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映射中。使用 LRUMap 来自阿帕奇公地的人会这么做的。

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

    使用其他键

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

    总结

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

        5
  •  5
  •   oxbow_lakes    17 年前

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

    显然,这不会像您希望的那样处理异常,并且缓存中没有内置的逐出。不过,也许您可以使用它作为更改静态缓存类的基础。

        6
  •  3
  •   Vadzim    7 年前

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

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

    更新 :这样,当两个线程同时为同一个键进入同步段,但具有不同的锁时,就有可能出现竞争条件。

    所以使用起来更安全、更高效 expiring 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 是线程安全的,不会因为不同的密钥而并发读写。

        7
  •  2
  •   Alexander    17 年前

    您的主要问题不仅仅是可能有多个具有相同值的字符串实例。主要的问题是,为了访问StaticCache对象,您只需要在一个监视器上进行同步。否则,多个线程最终可能会并发地修改staticcache(尽管在不同的键下),这很可能不支持并发修改。

        8
  •  2
  •   Mario Ortegón    17 年前

    呼叫:

       final String key = "Data-" + email;
    

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

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

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

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

        9
  •  2
  •   kohlerm    17 年前

    使用合适的缓存框架,如 ehcache .

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

    关于string.intern()是内存泄漏源的注释,这实际上不是真的。 内部字符串 垃圾收集,这可能需要更长的时间,因为在某些JVM(Sun)上,它们存储在Perm空间中,而Perm空间只被完整的GC接触到。

        10
  •  2
  •   igor.zh    8 年前

    这个问题在我看来有点太宽泛了,因此它提出了同样宽泛的答案。所以我会尽力回答 the question 我已被重定向,很遗憾,其中一个已作为副本关闭。

    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 操作获取(内部)锁以在短时间内独占访问映射,如果对应的对象已经在映射中,则当前线程将等待, 否则它会把新的 Condition 在地图上,松开(内部)锁并继续, 并认为获得了(外)锁。 (外) unlock 首先获取(内部)锁的操作将发出打开信号 条件 然后从地图中删除对象。

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

    请注意,语义 lock() 这个类的方法不同于 ReentrantLock.lock() ,重复 锁定() 未配对的调用 unlock() 将无限期挂起当前线程。

    一个可能适用于这种情况的用法示例,操作描述

        ValueLock<String> lock = new ValueLock<String>();
        // ... share the lock   
        String email = "...";
        try {
            lock.lock(email);
            //... 
        } finally {
            lock.unlock(email);
        }
    
        11
  •  1
  •   Smern    12 年前

    这相当晚了,但是这里有很多错误的代码。

    在本例中:

    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而不是putifaste),那么尽管使用了同步映射,操作将没有必要的同步。注意使用putifastence带来的复杂性:要么,即使在不需要的情况下(因为在检查缓存内容之前,Put无法知道是否需要Put值),也必须计算出Put值,要么需要小心使用委托(例如,使用Future,它有效,但有点不匹配;请参见下文)。如有需要,按需取得看跌价值。

    期货的使用是有可能的,但似乎相当尴尬,而且可能有点过于工程化。未来的API是异步操作的核心,特别是对于可能无法立即完成的操作。涉及到未来很可能会增加一层线程创建——额外的可能不必要的复杂性。

    这种类型的操作使用Future的主要问题是,Future内在地与多线程联系在一起。在不需要新线程的情况下使用future意味着忽略了很多future机制,使其成为这种使用的过于沉重的API。

        12
  •  0
  •   Matthias Winkelmann    17 年前

    为什么不呈现一个静态的HTML页面,该页面将被提供给用户,并每隔X分钟重新生成一次?

        13
  •  0
  •   John Gardner    17 年前

    如果您不需要,我还建议您完全取消字符串连接。

    final String key = "Data-" + email;
    

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

    如果不是的话,我就这么做

    final String key = email;
    

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

        14
  •  0
  •   celen    9 年前

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

    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
            }
        }
    }
    
        15
  •  0
  •   ragnaroh    8 年前

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

    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);
                }
            }
        }
    }
    
        16
  •  0
  •   AlikElzin-kilaka planben    8 年前

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

    参见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:

    公共类dynamickelock实现锁 { 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();
            }
        }
    }