【问题标题】:Use of a key to synchronize access to code block使用密钥同步访问代码块
【发布时间】:2017-06-22 01:18:50
【问题描述】:

通常我会锁定一个关键部分,如下所示。

public class Cache {
    private Object lockObject = new Object();

    public Object getFromCache(String key) {
        synchronized(lockObject) {

            if (cache.containsKey(key)) {
                // key found in cache - return cache value
            }
            else {
                // retrieve cache value from source, cache it and return
            }
        }
    }
}

我的想法是避免可能导致数据源被多次命中并且键被多次添加到缓存中的竞争条件。

现在,如果两个线程几乎同时进入 不同 缓存键,我仍然会阻止一个。

假设钥匙是唯一的 - 通过锁定钥匙,锁仍然可以工作吗?

我认为它不会起作用,因为我知道对象引用应该相同才能使锁生效。我想这归结为它如何检查相等性。

public class Cache {

    public Object getFromCache(String key) {
        synchronized(key) {

            if (cache.containsKey(key)) {
                // key found in cache - return cache value
            }
            else {
                // retrieve cache value from source, cache it and return
            }
        }
    }
}

【问题讨论】:

  • 没有。您需要保护的资源是缓存,而不是密钥。你的建议没有意义。
  • 通常,密钥会散列到锁定对象数组中以避免实例问题。这称为条带化,其中条带(资源)受到此锁的保护,不会发生任何突变。在您的缓存示例中,这称为记忆化以避免cache stampede,通过锁定内部哈希表条目来完成。为了避免锁定读取,这将使用乐观获取和回退(双重检查锁定)。 Caffeine 和其他缓存实现了这种技术。
  • 感谢 Ben,您的解释很有用。我将介绍一个散列在密钥上的锁数组。

标签: java concurrency thread-safety


【解决方案1】:
public class Cache {

    private static final Set<String> lockedKeys = new HashSet<>();

    private void lock(String key) throws InterruptedException {
        synchronized (lockedKeys) {
            while (!lockedKeys.add(key)) {
                lockedKeys.wait();
            }
        }
    }

    private void unlock(String key) {
        synchronized (lockedKeys) {
            lockedKeys.remove(key);
            lockedKeys.notifyAll();
        }
    }

    public Object getFromCache(String key) throws InterruptedException {
        try {
            lock(key);

            if (cache.containsKey(key)) {
                // key found in cache - return cache value
            }
            else {
                // retrieve cache value from source, cache it and return
            }

        } finally {
            unlock(key);
        }
    }

}
  • try-finally - 非常重要 - 即使您的操作抛出异常,您也必须保证在操作后解锁等待线程。
  • 如果您的后端分布在多个服务器/JVM上,它将无法工作。

【讨论】:

    【解决方案2】:

    每个对象都有一个隐式监视器,同步工作在该监视器上进行。字符串对象可以在堆中创建,也可以对于同一组字符(如果使用new 创建)不同,也可以来自池。只有当它们在同一个对象上同步时,两个线程才会使用同步块访问临界区。

    在字符串文字上同步确实是个坏主意。池中的字符串文字是共享的。试想一下,如果在代码的两个不同部分,您有两个同步部分,并且您在 String 的两个引用上同步,但使用具有相同字符集的字符串初始化,如果使用来自池的 String,那么这两个地方将是同一个对象.即使这两个地方可能有不同的业务环境,但您仍然可能最终导致您的应用程序被挂起。调试起来也会很困难。

    对于特定的问题,如果对键进行同步,是否可以解决目的。

    您希望避免两个线程在未读取缓存的最新值的情况下尝试写入。每个条目都有不同的密钥。假设一个线程 1 和线程 2 想要访问同一个键,那么在同一个键对象上的同步将阻止他们两个进入同步块。同时,如果 thread3 想要访问另一个不同的密钥,那么它可以很好地做到这一点。在这里,我们看到与所有键的读取和写入的单个公共对象相比,读取和写入速度更快。到目前为止一切顺利,但是如果假设您保留一个数组或任何其他类似的非线程安全数据结构来存储缓存值,则会出现问题。同时写入(对于两个或多个不同的键)可能导致一个写入被同一索引处的另一个写入覆盖。

    因此,这取决于缓存数据结构的实现,您如何最好地准备它以在多线程环境中更快地读取和写入。

    【讨论】:

    • 不知道为什么这被否决了,但我认为它证实了我的期望,即实例必须相同。
    • @awi 我投了反对票,因为它没有解决问题,这就是我在评论中所说的。如果您认为这是对您实际所问问题的正确答案,您将是错误的。
    • @avvi 在某种程度上我自己同意 EJP,在重新审视它之后,我发现我之前错过了一些东西。我已经编辑了答案并尝试触摸之前错过的区域。
    • @EJP 是的,在审查中我同意您的评论,即按规定使用密钥没有意义,但我不同意它完全没有意义。重要的是实施。我的问题的本质不是关于缓存——那是关键部分的例子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-21
    • 1970-01-01
    • 2016-02-27
    • 2018-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多