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