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