gpt4 book ai didi

java - 在 Java 中同步 String 对象

转载 作者:IT老高 更新时间:2023-10-28 20:42:07 25 4
gpt4 key购买 nike

我有一个 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 服务。过期了,我想我需要同步缓存获取/设置操作,而且使用缓存键似乎是同步对象的一个​​很好的候选者(这样,对电子邮件 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;
}
我还为诸如“同步块(synchronized block)之前”、“同步块(synchronized block)内部”、“即将离开同步块(synchronized block)”和“同步块(synchronized block)之后”之类的内容添加了日志记录行,因此我可以确定我是否有效地同步了 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 操作进入/退出同步块(synchronized block)。
在 String 对象上同步是否存在问题?我认为缓存键是一个不错的选择,因为它是操作所独有的,即使 final String key在方法中声明,我认为每个线程都会获得对同一个对象的引用,因此会在这个单个对象上进行同步。
我在这里做错了什么?
更新 : 进一步查看日志后,似乎方法具有相同的同步逻辑,其中 key 始终相同,例如
final String key = "blah";
...
synchronized(key) { ...
不要表现出相同的并发问题 - 一次只有一个线程进入 block 。
更新 2 : 感谢大家的帮助!我接受了关于 intern() 的第一个答案ing 字符串,它解决了我最初的问题 - 多个线程正在进入我认为不应该进入的同步块(synchronized block),因为 key具有相同的值。
正如其他人所指出的,使用 intern()出于这样的目的,在这些字符串上进行同步确实证明是一个坏主意 - 当对 web 应用程序运行 JMeter 测试以模拟预期负载时,我看到使用的堆大小在不到 20 分钟的时间内增长到近 1GB。
目前我正在使用仅同步整个方法的简单解决方案 - 但我 真的像 martinprobst 和 MBCook 提供的代码示例,但因为我有大约 7 个类似的 getData()目前这个类中的方法(因为它需要来自 Web 服务的大约 7 条不同的数据),我不想为每个方法添加关于获取和释放锁的几乎重复的逻辑。但这对于 future 的使用绝对是非常非常有值(value)的信息。我认为这些最终是关于如何最好地使这种线程安全的操作的正确答案,如果可以的话,我会为这些答案投更多票!

最佳答案

没有让我的大脑完全投入,从快速扫描你所说的看起来好像你需要实习()你的字符串:

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锁都忘记,在整个操作周围放一个大的synchronized(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 期间进行,因此这可能不是问题。

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

    关于java - 在 Java 中同步 String 对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/133988/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com