get()在Guava的缓存中是线程安全的操作吗?

seg*_*egu 3 java caching guava

我发现使用 CacheLoader 操作的 put 和 get 在底层使用了可重入锁,但为什么 getIfPresent 操作没有实现这一点?

get 由 getIfPresent 使用

@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();
            }
        }
Run Code Online (Sandbox Code Playgroud)

 @Nullable
        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            this.lock();
           .....
Run Code Online (Sandbox Code Playgroud)

在基本 get/put 操作中实现线程安全的唯一方法是在客户端上使用同步吗?

rzw*_*oot 5

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

\n

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

\n

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

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

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

\n

这是一个完美的例子:

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

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

\n

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

\n
    \n
  • 询问用户他们想要多少钱(例如,\xe2\x82\xac50,-)。
  • \n
  • 从“线程安全”检索帐户余额Map<Account, Integer>(将帐户 ID 映射到帐户中的美分)。
  • \n
  • 检查是否\xe2\x82\xac50,-。如果不是,则显示错误。如果是...
  • \n
  • 吐出\xe2\x82\xac50,-,并用 更新线程安全映射.put(acct, balance - 5000)
  • \n
\n

一切都完美线程安全。然而,这将会变得非常非常错误 - 如果用户在银行通过柜员机取款的同时使用他们的卡,那么银行或用户都会在这里变得非常幸运。我希望很明显可以看出如何以及为什么。

\n

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

\n

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

\n
start some sort of transaction;\nfetch account balance;\ndeal with insufficient funds;\nspit out cash;\nupdate account balance;\nend transaction;\n
Run Code Online (Sandbox Code Playgroud)\n

现在 guava 的代码就很有意义了:

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

    \n
  • \n
  • 相反,如果您想要这个常见操作:“获取缓存值。但是,如果它不可用,则运行此代码来计算缓存值,缓存结果,然后将其返回给我。此外,请确保 if 2线程同时最终运行这个精确的操作,只有一个线程运行计算,另一个线程将等待另一个线程(不要说“第一个”,这不是你应该如何看待线程的方式)完成,然后使用该结果”..然后,使用正确的调用:.cache.get(key, k -> calculateValueForKey(k)). 正如文档明确指出的那样,这将等待另一个也在“加载”该值的线程(这就是番石榴缓存所谓的计算过程)。

    \n
  • \n
  • 无论您从 Cache API 调用什么,您都无法“破坏它”,从某种意义上说,我破坏了该 HashMap。缓存 API 部分通过使用锁(例如ReentrantLock对其进行变异操作)来完成此操作,部分通过在ConcurrentHashMap后台使用。

    \n
  • \n
\n

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

\n

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

\n