有一个webapp,我正在进行一些负载/性能测试,特别是在我们希望几百个用户访问同一页面并在此页面上每10秒钟点击刷新的功能上。我们发现我们可以使用此功能进行改进的一个方面是在一段时间内缓存来自Web服务的响应,因为数据没有变化。
在实现这个基本缓存之后,在一些进一步的测试中,我发现我没有考虑并发线程如何同时访问Cache。我发现在大约100ms的时间内,大约有50个线程试图从Cache中获取对象,发现它已经过期,命中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操作。
然而,这似乎并没有起作用。我的测试日志输出如下:
(日志输出是'threadname''记录器名''消息') http-80-Processor253 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor253 jsp.view-page - getSomeDataForEmail:内部同步块 http -80-Processor253 cache.StaticCache - get:key at [SomeData-test@test.com]已过期 http-80-Processor253 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor263 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor263 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor263 cache.StaticCache - get:object at key [SomeData -test@test.com]已过期 http-80-Processor263 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor131 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor131 jsp .view-page - getSomeDataForEmail:内部同步块 http-80-Processor131 cache.StaticCache - get:object at key [SomeData-test@test.com]已过期 http-80-Processor131 cache.StaticCache - get:key [SomeData- test@test.com]返回值[null] http-80-Processor104 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor104 cache.StaticCache - get:object at key [SomeData-test@test.com]已过期 http-80-Processor104 cache.StaticCache - get:key [SomeData-test@test.com]返回值[null] http-80-Processor252 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor283 jsp .view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor2 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor2 jsp.view-page - getSomeDataForEmail:内部同步块
我希望在get / set操作周围一次只能看到一个线程进入/退出同步块。
在String对象上同步是否存在问题?我认为缓存键是一个很好的选择,因为它对于操作是唯一的,即使final String key
在方法中声明,我也认为每个线程都会获得对同一个对象的引用,因此会同步单个对象。
我在这做错了什么?
更新:在进一步查看日志之后,似乎具有相同同步逻辑的方法,其中密钥始终相同,例如
final String key = "blah";
...
synchronized(key) { ...
不会出现相同的并发问题 - 一次只有一个线程进入该块。
更新2:感谢大家的帮助!我接受了关于intern()
字符串的第一个答案,它解决了我的初始问题 - 多个线程进入同步块,我认为它们不应该,因为它key
具有相同的值。
正如其他人已经指出的那样,使用intern()
这样的目的并同步这些字符串确实是一个坏主意 - 当针对webapp运行JMeter测试来模拟预期的负载时,我看到使用的堆大小增长到接近1GB不到20分钟。
发布于 2018-10-22 12:48:45
其他人建议实习字符串,这将有效。
问题是Java必须保持内部字符串。我被告知即使你没有持有引用也会这样做,因为下次有人使用该字符串时,该值必须相同。这意味着实习所有字符串可能会开始占用内存,而你所描述的负载可能是一个大问题。
我已经看到了两个解决方案:
您可以在另一个对象上进行同步
而不是使用电子邮件,创建一个包含电子邮件(例如User对象)的对象,该电子邮件将电子邮件的值保存为变量。如果您已经有另一个代表该人的对象(比如您已经根据他们的电子邮件从数据库中提取了某些内容),那么您可以使用它。通过实现equals方法和hashcode方法,您可以确保Java在执行静态cache.contains()时认为对象是相同的,以确定数据是否已经在缓存中(您必须在缓存上进行同步) )。
实际上,您可以为要锁定的对象保留第二个Map。像这样的东西:
Map<String, Object> emailLocks = new HashMap<String, Object>();
Object lock = null;
synchronized (emailLocks) {
lock = emailLocks.get(emailAddress);
if (lock == null) {
lock = new Object();
emailLocks.put(emailAddress, lock);
}
}
synchronized (lock) {
// See if this email is in the cache
// If so, serve that
// If not, generate the data
// Since each of this person's threads synchronizes on this, they won't run
// over eachother. Since this lock is only for this person, it won't effect
// other people. The other synchronized block (on emailLocks) is small enough
// it shouldn't cause a performance problem.
}
这样可以防止在同一个电子邮件地址上进行15次提取。您需要一些东西来阻止太多条目在emailLocks映射中结束。使用来自Apache Commons的LRUMap可以做到这一点。
这需要一些调整,但它可以解决您的问题。
使用其他密钥
如果您愿意忍受可能的错误(我不知道这有多重要),您可以使用String的哈希码作为密钥。整理不需要实习。
摘要
我希望这有帮助。线程很有趣,不是吗?您还可以使用会话设置一个值,意思是“我已经在努力找到这个”并检查是否需要尝试创建第二个(第三个,第N个)线程或者只是等待结果显示在缓存中。我想我有三个建议。
发布于 2018-10-22 14:30:56
字符串不适合同步。如果必须同步字符串ID,可以使用字符串创建互斥锁(请参阅“ 同步ID ”)。该算法的成本是否值得,取决于调用您的服务是否涉及任何重要的I / O.
也:
https://stackoverflow.com/questions/-100000870
复制相似问题