mat*_*t b 42 java multithreading synchronization synchronized thread-safety
我有一个webapp,我正在进行一些负载/性能测试,特别是在我们希望有几百个用户访问同一页面并在此页面上每10秒点击一次刷新的功能.我们发现我们可以使用此功能进行改进的一个方面是在一段时间内缓存来自Web服务的响应,因为数据没有变化.
在实现这个基本缓存之后,在一些进一步的测试中,我发现我没有考虑并发线程如何同时访问Cache.我发现在大约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;
}
Run Code Online (Sandbox Code Playgroud)
因此,为了确保在对象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;
}
Run Code Online (Sandbox Code Playgroud)
我还为"同步块之前","内部同步块","即将离开同步块"和"同步块之后"之类的内容添加了日志行,因此我可以确定是否有效地同步了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 key [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) { ...
Run Code Online (Sandbox Code Playgroud)
不会出现相同的并发问题 - 一次只有一个线程进入该块.
更新2:感谢大家的帮助!我接受了关于intern()
字符串的第一个答案,它解决了我的初始问题 - 多个线程进入同步块,我认为它们不应该,因为它key
具有相同的值.
正如其他人所指出的那样,使用intern()
这样的目的并同步这些字符串确实是一个坏主意 - 当针对webapp运行JMeter测试来模拟预期的负载时,我看到使用的堆大小增长到接近1GB不到20分钟.
目前我正在使用仅仅同步整个方法的简单解决方案 - 但我非常喜欢martinprobst和MBCook提供的代码示例,但由于我getData()
目前在这个类中有大约7个类似的方法(因为它需要大约7个不同的数据)从Web服务),我不想添加几乎重复的逻辑来获取和释放每个方法的锁.但这对于未来的使用来说绝对是非常非常有价值的信息.我认为这些最终是关于如何最好地进行这种线程安全的操作的正确答案,如果可以的话,我会给这些答案更多的投票!
Ste*_*sop 39
没有把我的大脑完全放入装备,从快速扫描你所说的内容看起来好像你需要实习()你的字符串:
final String firstkey = "Data-" + email;
final String key = firstkey.intern();
Run Code Online (Sandbox Code Playgroud)
具有相同值的两个字符串不一定是相同的对象.
请注意,这可能会引入新的争用点,因为在VM的深处,intern()可能必须获取锁.我不知道现代虚拟机在这个领域是什么样的,但人们希望它们能够进行极端优化.
我假设您知道StaticCache仍然需要是线程安全的.但是,如果你在调用getSomeDataForEmail时锁定缓存而不仅仅是密钥,那么与那里的争论相比应该是微不足道的.
对问题更新的回复:
我认为这是因为字符串文字总是产生相同的对象.戴夫·科斯塔在评论中指出它甚至比这更好:一个文字总是产生规范表示.因此,程序中任何位置具有相同值的所有String文字都将产生相同的对象.
编辑
其他人指出,实习生字符串的同步实际上是一个非常糟糕的主意 - 部分原因是允许创建实习字符串使其永久存在,部分原因是如果程序中任何地方的代码不止一位在实习字符串上同步,你有这些代码之间的依赖关系,防止死锁或其他错误可能是不可能的.
在我输入的其他答案中,正在开发通过为每个键字符串存储锁定对象来避免这种情况的策略.
这是一个替代方案 - 它仍然使用单一锁,但我们知道无论如何我们将需要其中一个用于缓存,而你谈论的是50个线程,而不是5000个,所以这可能不是致命的.我还假设这里的性能瓶颈是在DoSlowThing()中阻塞I/O很慢,因此不会被序列化.如果这不是瓶颈,那么:
显然,这种方法需要在使用前进行可靠性测试 - 我保证不会.
此代码不要求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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
每次将任何内容添加到缓存中时,所有线程都会唤醒并检查缓存(无论它们处于什么密钥之后),因此可以通过较少争用的算法获得更好的性能.但是,大部分工作将在I/O上大量空闲CPU时间阻塞期间进行,因此可能不是问题.
如果为缓存及其关联的锁定定义合适的抽象,返回的数据,IN_PROGRESS虚拟以及执行速度慢的操作,则可以将此代码用于多个缓存.将整个事物滚动到缓存上的方法可能不是一个坏主意.
Mar*_*bst 27
在intern'd String上进行同步可能根本不是一个好主意 - 通过实习,String变成一个全局对象,如果你在应用程序的不同部分同步interned字符串,你可能会变得非常奇怪,基本上是不可解决的同步问题,例如死锁.这似乎不太可能,但是当它发生时,你真的被搞砸了.作为一般规则,只能在本地对象上进行同步,您绝对确定模块外部的代码不会锁定它.
在您的情况下,您可以使用同步哈希表来存储密钥的锁定对象.
例如:
Object data = StaticCache.get(key, ...);
if (data == null) {
Object lock = lockTable.get(key);
if (lock == null) {
// we're the only one looking for this
lock = new Object();
synchronized(lock) {
lockTable.put(key, lock);
// get stuff
lockTable.remove(key);
}
} else {
synchronized(lock) {
// just to wait for the updater
}
data = StaticCache.get(key);
}
} else {
// use from cache
}
Run Code Online (Sandbox Code Playgroud)
此代码具有竞争条件,其中两个线程可能会将对象彼此放入锁定表中.这应该不是问题,因为那时你只有一个线程调用webservice并更新缓存,这应该不是问题.
如果您在一段时间后使缓存失效,则应在从锁定!= null情况下从缓存中检索数据后再检查数据是否为空.
或者,更容易,您可以使整个缓存查找方法("getSomeDataByEmail")同步.这意味着所有线程在访问缓存时都必须进行同步,这可能是性能问题.但是一如既往,首先尝试这个简单的解决方案,看看它是否真的是一个问题!在许多情况下它不应该是,因为您可能花费更多时间处理结果而不是同步.
McD*_*ell 11
字符串不适合同步.如果必须同步字符串ID,可以使用字符串创建互斥锁(请参阅" 同步ID ").该算法的成本是否值得,取决于调用您的服务是否涉及任何重要的I/O.
也:
其他人建议实习字符串,这将有效.
问题是Java必须保持内部字符串.有人告诉我,即使你没有持有引用也会这样做,因为下次有人使用该字符串时,该值必须相同.这意味着实习所有字符串可能会开始占用内存,而你所描述的负载可能是一个大问题.
我已经看到了两个解决方案:
您可以在另一个对象上进行同步
而不是使用电子邮件,创建一个包含电子邮件(例如用户对象)的对象,该电子邮件将电子邮件的值保存为变量.如果您已经有另一个代表该人的对象(比如您已根据他们的电子邮件从数据库中提取了某些内容),则可以使用该对象.通过实现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.
}
Run Code Online (Sandbox Code Playgroud)
这样可以防止同一个电子邮件地址中的15次提取.您需要一些东西来阻止太多条目在emailLocks地图中结束.使用Apache Commons的LRUMap可以做到这一点.
这需要一些调整,但它可以解决您的问题.
使用其他密钥
如果您愿意忍受可能的错误(我不知道这有多重要),您可以使用String的哈希码作为键.整理不需要实习.
摘要
我希望这有帮助.线程很有趣,不是吗?您还可以使用会话来设置一个值,意思是"我已经在努力找到这个"并检查是否需要尝试创建第二个(第三个,第N个)线程或者只是等待结果显示在缓存中.我想我有三个建议.
您可以使用1.5并发实用程序来提供旨在允许多个并发访问的缓存,以及单个添加点(即只有一个线程执行昂贵的对象"创建"):
private ConcurrentMap<String, Future<SomeData[]> cache;
private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {
final String key = "Data-" + email;
Callable<SomeData[]> call = new Callable<SomeData[]>() {
public SomeData[] call() {
return service.getSomeDataForEmail(email);
}
}
FutureTask<SomeData[]> ft; ;
Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
if (f == null) { //this means that the cache had no mapping for the key
f = ft;
ft.run();
}
return f.get(); //wait on the result being available if it is being calculated in another thread
}
Run Code Online (Sandbox Code Playgroud)
显然,这并不像你想要的那样处理异常,并且缓存没有内置的驱逐.也许你可以用它作为改变你的StaticCache类的基础.
这是一个安全的Java 8简短解决方案,它使用专用锁定对象的映射进行同步:
private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
}
return data;
}
Run Code Online (Sandbox Code Playgroud)
缺点是键和锁定对象将永远保留在地图中。
可以这样解决:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
try {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
} finally {
keyLocks.remove(key); // vulnerable to race-conditions
}
}
return data;
}
Run Code Online (Sandbox Code Playgroud)
但是,随后流行的密钥将不断地重新插入到地图中,并重新分配锁定对象。
更新:当两个线程同时为同一密钥但具有不同锁的同步部分进入同步段时,这就留下了竞争条件的可能性。
因此,使用到期的Guava Cache可能更安全有效:
private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
.build(CacheLoader.from(Object::new));
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
synchronized (keyLocks.getUnchecked(key)) {
SomeData[] data = StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data);
}
}
return data;
}
Run Code Online (Sandbox Code Playgroud)
请注意,这里假设它StaticCache
是线程安全的,并且不会因并发读写不同键而受到影响。
归档时间: |
|
查看次数: |
40391 次 |
最近记录: |