【问题标题】:Is get() thread-safe operation in Guava's cache?番石榴缓存中的 get() 线程安全操作吗?
【发布时间】:2022-01-10 14:36:39
【问题描述】:

我发现使用 CacheLoader 的 put 和 get 操作在后台使用 Reentrant 锁,但为什么 getIfPresent 操作没有实现呢?

getIfPresent 使用的get

@Nullable
        V get(Object key, int hash) {
            try {
                if (this.count != 0) {
                    long now = this.map.ticker.read();
                    ReferenceEntry<K, V> e = this.getLiveEntry(key, hash, now);
                    Object value;
                    if (e == null) {
                        value = null;
                        return value;
                    }

                    value = e.getValueReference().get();
                    if (value != null) {
                        this.recordRead(e, now);
                        Object var7 = this.scheduleRefresh(e, e.getKey(), hash, value, now, this.map.defaultLoader);
                        return var7;
                    }

                    this.tryDrainReferenceQueues();
                }

                Object var11 = null;
                return var11;
            } finally {
                this.postReadCleanup();
            }
        }

 @Nullable
        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            this.lock();
           .....

为了在基本的 get/put 操作中实现线程安全,我唯一能做的就是在客户端上使用同步吗?

【问题讨论】:

  • 独占访问需要锁定才能进行修改。一次读取可以作为内存屏障来查看最新值,因此它可以是无锁的,以避免多个读取器导致争用的成本。这需要特别小心,以免读者看到多个字段的部分写入(例如corrupted list walk)。同样ConcurrentHashMap 做同样的事情,get(key) 没有被并发调用put(key, value) 阻塞。这是性能优化。

标签: java caching guava


【解决方案1】:

即使getIfPresent 确实使用了锁,那也无济于事。它比这更基本。

让我换个说法:定义“线程安全”。

以下是非线程安全实现中可能发生的情况的示例:

  • 您在普通的 jane j.u.HashMap 上调用 .put,而不持有任何锁。
  • 同时,另一个线程也执行此操作。
  • 地图现在处于损坏状态。如果您遍历元素,则第一个 put 语句根本不显示,第二个 put 语句出现在您的迭代中,并且完全不相关的键消失了。但是使用第二个线程的键在该映射上调用.get(k) 并没有找到它,即使它在.entrySet() 中返回。这是没有意义的,并且违反了 j.u.HashMap 的所有规则。除了“我不是线程安全的”之外,hashmap 的规范没有解释任何这些。

这是一个非线程安全的例子。

这是一个完美的例子:

  • 2 个线程开始。
  • 一些外部事件(例如日志)显示线程 1 非常非常非常稍微领先于线程 2,但如果相关的“领先”概念,则意味着您的代码已损坏。这不是多核的工作原理。
  • 线程 1 将事物添加到支持并发的映射中,并记录它已这样做。
  • 线程 2 记录它开始操作。 (从您观察到的几件事来看,它似乎稍微“稍后”运行)所以我想我们“在” T1 添加该事物的点之后)现在查询该事物并且 没有得到结果.1

没关系。那仍然是线程安全的。线程安全并不意味着与该数据类型的实例的每次交互都可以理解为“首先发生这件事,然后发生那件事”。想要这样是非常成问题,因为计算机能够真正为您提供这种保证的唯一方法是禁用除单个内核之外的所有内核并非常缓慢地运行所有内容。缓存的目的是加快速度,而不是减慢速度!

这里缺乏保证的问题是,如果你对同一个对象运行多个单独的操作,你就会遇到麻烦。以下是银行 ATM 机的一些伪代码,从长远来看会出现严重错误:

  • 询问用户他们想要多少钱(例如,50 欧元,-)。
  • 从“线程安全”Map&lt;Account, Integer&gt; 检索帐户余额(将帐户 ID 映射到帐户中的美分)。
  • 检查是否 50 欧元,-。如果否,则显示错误。如果是...
  • 吐出 50 欧元,-,并使用 .put(acct, balance - 5000) 更新线程安全映射。

一切都是完全线程安全的。然而这将是非常非常错误的——如果用户同时使用他们的卡,他们在银行通过柜员取款,无论是银行还是用户都会在这里变得非常幸运。我希望很明显地知道如何以及为什么。

结果是:如果您在操作之间存在依赖关系,那么您无法使用“线程安全”概念来解决它;唯一的方法是实际编写明确标记这些依赖关系的代码

编写该银行代码的唯一方法是使用某种形式的锁定。基本锁定或乐观锁定,无论哪种方式都很好,但某种锁定。它必须看起来像2

start some sort of transaction;
fetch account balance;
deal with insufficient funds;
spit out cash;
update account balance;
end transaction;

现在番石榴的代码很有意义:

  • 没有“早”和“晚”之类的东西。您需要停止以这种方式考虑多核。除非您明确编写建立这些东西的原语。缓存接口确实有这些。使用正确的操作! getIfPresent 如果您当前的线程可以获取该数据,将为您提供缓存。如果不是,则返回null,这就是调用所做的

  • 如果你想要这个通用操作:“获取我缓存的值。但是,如果它不可用,则运行此代码来计算缓存值,缓存结果并将其返回给我。另外,确保如果 2 个线程同时最终运行这个确切的操作,只有一个线程运行计算,另一个将等待另一个(不要说“第一个”,这不是你应该如何考虑线程)完成,并使用该结果”.. 然后,使用正确的调用:.cache.get(key, k -&gt; calculateValueForKey(k))。作为the docs explicitly call out,这将等待另一个线程“加载”值(这就是番石榴缓存调用的计算过程)。

  • 无论您从 Cache API 调用什么,都不能“破坏”它,因为我破坏了那个 HashMap。缓存 API 部分通过使用锁(例如 ReentrantLock 对其进行变异操作),部分通过在后台使用 ConcurrentHashMap 来完成此操作。

[1] 通常,日志框架最终会在进程中注入一个实际的显式锁定,因此在这种情况下您确实经常得到保证,但由于日志框架,这只是“偶然”。这不是保证(例如,您可能正在登录到单独的日志文件!)而且您“见证”的内容通常可能是谎言。例如,也许您有 2 个日志语句,它们都记录到单独的文件(并且根本不相互锁定),并且它们将时间戳记录为日志的一部分。事实上,一个日志行说 '12:00:05' 而另一个说 '12:00:06' 没有任何意义 - 日志线程获取当前时间,创建一个描述消息的字符串,并告诉操作系统将其写入文件。您显然无法保证 2 个日志线程以相同的速度运行。也许一个线程获取时间(12:00:05),创建字符串,想要写入磁盘但操作系统在写入完成之前切换到另一个线程,另一个线程是另一个记录器,它读取时间( 12:00:06),创建字符串,将其写出,完成,然后第一个记录器继续,写入其上下文。多田: 2 个线程,您“观察”一个线程“较早”,但这是不正确的。也许这个例子会进一步强调为什么以哪个“第一个”来考虑线程会导致你错了。

[2] 此代码具有额外的复杂性,即您正在与不能事务性的系统进行交互。交易的重点是您可以中止它;您不能中止用户从 ATM 取款。你通过记录你即将吐出钱来解决这个问题,然后吐出钱,然后记录你已经吐出钱。最后写入这个日志,说明它已经在用户的账户余额中处理了。其他代码需要检查此日志并采取相应措施。例如,在启动时,银行的 DB 机器需要标记“悬空”的 ATM 交易,并且必须让人工检查视频源。解决了银行DB机器刚要取钞时有人绊倒银行DB机器电源线的问题。

【讨论】:

    【解决方案2】:

    似乎番石榴缓存正在实现 ConcurrentMap api

    class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> 
    

    所以基本的 get 和 put 操作本质上应该是线程安全的

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多