我有我在做一些负载/性能测试上,particularily上我们预计几百个用户要访问同一页面,打此页面上每10秒刷新一个特点中间的Web应用程序。 我们发现,我们可以用此功能进行改进的一个领域是缓存从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服务时,在对象key
到期,我想我需要同步缓存获取/设置操作,并且它似乎像使用缓存键是一个对象的一个很好的候选要同步的(这种方式,调用此方法电子邮件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页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor253 jsp.view页 - getSomeDataForEmail:同步块内
HTTP-80-Processor253 cache.StaticCache - 获得:在关键的对象[SomeData-test@test.com]已过期
HTTP-80-Processor253 cache.StaticCache - 获得:键[SomeData-test@test.com]返回值[空]
HTTP-80-Processor263 jsp.view页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor263 jsp.view页 - getSomeDataForEmail:同步块内
HTTP-80-Processor263 cache.StaticCache - 获得:在关键的对象[SomeData-test@test.com]已过期
HTTP-80-Processor263 cache.StaticCache - 获得:键[SomeData-test@test.com]返回值[空]
HTTP-80-Processor131 jsp.view页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor131 jsp.view页 - getSomeDataForEmail:同步块内
HTTP-80-Processor131 cache.StaticCache - 获得:在关键的对象[SomeData-test@test.com]已过期
HTTP-80-Processor131 cache.StaticCache - 获得:键[SomeData-test@test.com]返回值[空]
HTTP-80-Processor104 jsp.view页 - getSomeDataForEmail:同步块内
HTTP-80-Processor104 cache.StaticCache - 获得:在关键的对象[SomeData-test@test.com]已过期
HTTP-80-Processor104 cache.StaticCache - 获得:键[SomeData-test@test.com]返回值[空]
HTTP-80-Processor252 jsp.view页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor283 jsp.view页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor2 jsp.view页 - getSomeDataForEmail:即将进入同步块
HTTP-80-Processor2 jsp.view页 - getSomeDataForEmail:同步块内
我想只看到一次一个线程进入/退出周围的get / set操作同步块。
是否有对String对象同步的问题吗? 我想缓存键将是一个不错的选择,因为它是唯一的操作,并且即使final String key
在方法中声明,我在想,每个线程会得到同一个对象的引用,因此会此单个对象上的同步。
我在做什么错在这里?
更新 :在日志进一步看后,好像与这里的关键是始终不变的相同同步逻辑的方法,如
final String key = "blah";
...
synchronized(key) { ...
不要表现出相同的并发问题 - 只有一个线程在同一时间正在进入该块。
更新2:感谢大家的帮助! 我接受了关于第一个答案intern()
荷兰国际集团字符串,它解决了我最初的问题-在多个线程正在进入,我认为他们不应该synchronized块,因为key
的有同样的价值。
正如其他人所指出的那样,使用intern()
这样的目的和对这些字符串同步确实被证明是一个坏主意-运行JMeter的测试时,针对Web应用程序来模拟预期的负载,我看到使用的堆大小长到几乎有1GB不到20分钟。
目前我使用的只是同步整个方法的简单的解决方案-但我真的很喜欢通过martinprobst和MBCook提供的代码示例,但因为我有大约7类似getData()
这个类目前的方法(因为它大概需要7个不同从Web服务)的数据块,我不想增加有关获取和释放锁每种方法几乎重复的逻辑。 但是,这绝对是未来的使用非常非常有价值的信息。 我觉得这些都是最终对如何最好地做出这样的线程安全的操作正确的答案,如果我能我会给出更多的选票来回答这些问题!
Answer 1:
不把我的大脑完全进入齿轮,从你说的话,它看起来好像你需要实习生()的字符串快速扫描:
final String firstkey = "Data-" + email;
final String key = firstkey.intern();
两个字符串具有相同价值,否则不一定是同一个对象。
请注意,这可能引入竞争的一个新的起点,因为深的VM,实习生()可能会获得锁。 我不知道现代的虚拟机看起来像在这一领域,但人们希望他们被恶魔般的优化。
我想你知道,StaticCache仍然需要是线程安全的。 但是,争用,如果你锁定你不得不上的缓存,而不是仅仅在调用getSomeDataForEmail键相比应该有微小的。
应对问题的更新 :
我认为这是因为一个字符串文字总是得到相同的对象。 戴夫·科斯塔指出了一个评论,它比这更好的:文字始终得到规范的表示。 因此,在程序的任何地方相同值的字符串文字会产生相同的对象。
编辑
还有人指出, 关于实习生串同步实际上是一个非常糟糕的主意 -部分是因为创造实习生字符串被允许以使它们永远存在,部分原因是因为如果你的程序的任何地方的代码不止一个位在实习生串同步,你具有的代码那些比特之间的相关性,并防止死锁或其他错误是不可能的。
策略通过存储每个键的字符串的锁定对象,以避免这是在其他的答案被开发作为I型。
下面是一个另类 - 它依然采用的是单一的锁,但我们知道我们会需要那些高速缓存中的一个,无论如何,你都在谈论50个线程,而不是5000,所以这可能不是致命的。 我也假设了性能瓶颈,这里是将因此受益巨大不被序列化慢阻塞I / O在DoSlowThing()。 如果这不是瓶颈,那么:
- 如果CPU忙那么这种方法可能是不够的,你需要另一种方法。
- 如果CPU不忙,并访问服务器不是瓶颈,那么这种做法是矫枉过正,你不如忘记了这一点,每个钥匙锁,把一个大同步(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假人,慢执行的操作。 滚动整个事情到上高速缓存的方法可能不是一个坏主意。
Answer 2:
同步上intern'd字符串可能不是一个好主意,在所有 - 通过实习它,字符串变成一个全球性的对象,如果你在你的应用程序的不同部分在同一实习字符串同步,你可能会很奇怪和基本上undebuggable同步问题,如死锁。 这似乎不大可能,但是当它发生时你真的搞砸。 作为一般规则,永远只能同步本地对象,你绝对相信你的模块之外的任何代码可能会锁定它。
你的情况,你可以使用同步哈希表来存储对象锁定为你的钥匙。
例如:
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
}
这段代码有竞争状态,其中两个线程可能将物体放入锁表后,对方。 然而它不能是一个问题,因为你只能有一个更加线程调用web服务和更新缓存,这不应该是一个问题。
如果你正在失效一段时间后缓存,你应该检查从缓存中检索它,在锁定后的数据是否为空了!= NULL情况。
另外,和容易得多,你可以同步所有缓存查找方法(“getSomeDataByEmail”)。 这将意味着所有的线程都在访问高速缓存,这可能是一个性能问题同步。 但一如既往,先试试这个简单的解决方案,看看它是否是一个真正的问题! 在许多情况下,它不应该,因为你可能会花更多的时间在处理结果不是同步。
Answer 3:
字符串是不同步的良好候选者。 如果你必须在一个String ID同步,它可以通过使用字符串创建一个互斥量来完成(见“ 同步上的ID ”)。 无论是算法的代价是值得的,取决于是否调用您的服务涉及任何显著的I / O。
也:
- 我希望StaticCache.get()和set()方法是线程安全的。
- 中的String.intern()是有成本的(一个虚拟机的实现之间变化),并应小心使用。
Answer 4:
也有人建议实习的字符串,将工作。
问题是,Java有保持周围实习字符串。 有人告诉我,它这样做,即使你不持有引用,因为值必须是相同的,下一次有人使用该字符串。 这意味着实习所有字符串开始吃内存,这与你所描述的可能是一个大问题的负载。
我已经看到了两个解决办法:
你可以对另一个对象同步
取而代之的是电子邮件,使保存保存为变量电子邮件值的电子邮件(比如用户对象)的对象。 如果你已经有一个代表他的另一对象(说你已经根据他们的邮件拉东西从DB),你可以使用它。 通过实现equals方法和hashCode方法可以确保当你做一个静态cache.contains Java的考虑对象相同的()来找出如果数据已经在缓存中(你必须要在高速缓存同步)。
其实,你可以继续第二个地图的对象锁定。 事情是这样的:
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地图结束了太多的条目。 使用LRUMap从Apache的百科全书s就做到这一点。
这将需要一些调整,但它可能会解决你的问题。
使用不同的密钥
如果你愿意忍受可能出现的错误(我不知道这是多么重要),你可以使用String作为键的哈希码。 整数不需要被扣留。
摘要
我希望这有帮助。 线程是有趣的,不是吗? 你也可以使用会话设置值,意思是“我已经在努力寻找这种”和检查,看看如果第二(第三,第N)线程需要尝试创建或只是等待结果显示在缓存中。 我想我有三个建议。
Answer 5:
您可以使用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
}
显然,这不处理异常,你会想,和高速缓存中没有驱逐内置的。也许你可以使用它作为一个基础来改变你的StaticCache类,虽然。
Answer 6:
使用一个体面的缓存框架如的Ehcache 。
实现一个良好的缓存不是那么容易,因为有些人认为。
至于评论认为中的String.intern()是内存泄漏的来源,其实是不正确的。 实习字符串是垃圾回收,也许会需要更长的时间,因为在某些JVM的(SUN)它们存储在其中只接受全GC的感动烫发空间。
Answer 7:
下面是一个使用地图同步专用锁对象的安全简短的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;
}
它有一个缺点,即键和锁定的对象将保留在地图中永远。
这可以被工作围绕这样的:
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;
}
但当时流行的按键会被不断地重新插入地图与被重新分配锁定的对象。
更新 :这叶子当两个线程将同时输入相同的密钥,但用不同的锁同步,部分竞争条件的可能性。
因此,它可能会更安全,有效地使用即将到期的番石榴缓存 :
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;
}
需要注意的是它这里假设StaticCache
是线程安全的,不会遭受并发读取和写入不同的密钥。
Answer 8:
你的主要问题不仅仅是可能存在具有相同值的字符串的多个实例。 主要的问题是,你需要有上只有一个监视器用于访问StaticCache对象同步。 否则,多线程最后可能会同时修改StaticCache(虽然在不同的键),其中最有可能不支持并发修改。
Answer 9:
召唤:
final String key = "Data-" + email;
创建一个新对象每次方法被调用的时间。 因为该对象是你用什么来锁定,并且这种方法每次调用创建一个新对象,那么你是不是真的同步访问基于密钥的地图。
这进一步说明您的编辑。 当你有一个静态的字符串,然后它会工作。
使用实习生()解决了这个问题,因为它返回从由String类保持内部池,即确保了如果两个字符串相等,所述一个池中将被使用的字符串。 看到
http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()
Answer 10:
这个问题在我看来有点过于宽泛,因此策动同样广泛的答案。 所以,我会尽量回答这个问题我已经从重定向,可惜的是一个已关闭的重复。
public class ValueLock<T> {
private Lock lock = new ReentrantLock();
private Map<T, Condition> conditions = new HashMap<T, Condition>();
public void lock(T t){
lock.lock();
try {
while (conditions.containsKey(t)){
conditions.get(t).awaitUninterruptibly();
}
conditions.put(t, lock.newCondition());
} finally {
lock.unlock();
}
}
public void unlock(T t){
lock.lock();
try {
Condition condition = conditions.get(t);
if (condition == null)
throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
conditions.remove(t);
condition.signalAll();
} finally {
lock.unlock();
}
}
在(外) lock
操作(内部)获取锁去的地图独占访问的时间很短,如果对应的对象是已经在地图,当前线程等待,否则会提出了新的Condition
到地图时,释放(内)锁并继续,并且被认为是所获得的(外)锁。 (外) unlock
操作中,首先获取(内)锁定,将信号上Condition
,然后从地图中删除该对象。
该类不使用的并发版本Map
,因为它每次访问都是由单一的(内部)锁保护。
请注意,语义lock()
这个类的方法不同的是的ReentrantLock.lock()
反复lock()
调用无配对unlock()
将无限期挂起当前线程。
这可能是适用于使用情况的一个例子中,OP描述
ValueLock<String> lock = new ValueLock<String>();
// ... share the lock
String email = "...";
try {
lock.lock(email);
//...
} finally {
lock.unlock(email);
}
Answer 11:
这是比较晚的,但有相当这里提出了很多不正确的代码。
在这个例子:
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 /把API一个静态缓存,应该有周围get和getIfAbsentPut类型的操作至少同步,安全访问缓存。 同步的范围将是高速缓存本身。
如果更新必须对数据元素本身,即增加了同步的附加层,这应该是对各个数据元素制成。
SynchronizedMap来代替显式同步的使用,但仍必须遵守护理。 如果使用了错误的API(get和put代替的putIfAbsent),那么操作将没有必要的同步,尽管使用了同步映射。 注意利用的putIfAbsent引入的并发症:要么,看跌期权值必须即使在情况下,当不需要它(因为放不知道是否需要认沽值,直到缓存内容进行检查)计算,或者需要仔细使用委托的(比如说,使用未来,它的工作原理,但它是一个有点不匹配的;见下文),其中如果需要,按需获得认沽值。
利用期货是可能的,但似乎相当尴尬,或许有点过度设计的。 未来API是在它的异步操作的核心,特别是对于可能无法立即完成操作。 涉及未来极有可能增加线程创建的层 - 额外的可能不必要的并发症。
使用未来这种类型的操作的主要问题是,未来在多线程内在联系。 未来的用途时,一个新的线程是不是忽略了很多未来的机械,使其成为此使用过于沉重API必要手段。
Answer 12:
为什么不直接渲染被提供给用户和再生每隔x分钟一个静态HTML页面?
Answer 13:
我也建议摆脱字符串连接的全部,如果你不需要它。
final String key = "Data-" + email;
有没有在使用,你需要额外的“数据 - ”在关键的开头部分的电子邮件地址缓存中的其他东西/类型的对象?
如果不是,我只是使
final String key = email;
你避免所有这些额外的字符串创建了。
Answer 14:
String对象的其他方式同步:
String cacheKey = ...;
Object obj = cache.get(cacheKey)
if(obj==null){
synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
obj = cache.get(cacheKey)
if(obj==null){
//some cal obtain obj value,and put into cache
}
}
}
Answer 15:
如果别人有类似的问题,下面的代码工作,据我可以告诉:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class KeySynchronizer<T> {
private Map<T, CounterLock> locks = new ConcurrentHashMap<>();
public <U> U synchronize(T key, Supplier<U> supplier) {
CounterLock lock = locks.compute(key, (k, v) ->
v == null ? new CounterLock() : v.increment());
synchronized (lock) {
try {
return supplier.get();
} finally {
if (lock.decrement() == 0) {
// Only removes if key still points to the same value,
// to avoid issue described below.
locks.remove(key, lock);
}
}
}
}
private static final class CounterLock {
private AtomicInteger remaining = new AtomicInteger(1);
private CounterLock increment() {
// Returning a new CounterLock object if remaining = 0 to ensure that
// the lock is not removed in step 5 of the following execution sequence:
// 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
// 2) Thread 2 evaluates "v == null" to false in locks.compute
// 3) Thread 1 calls lock.decrement() which sets remaining = 0
// 4) Thread 2 calls v.increment() in locks.compute
// 5) Thread 1 calls locks.remove(key, lock)
return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
}
private int decrement() {
return remaining.decrementAndGet();
}
}
}
在OP的情况下,它会使用这样的:
private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
String key = "Data-" + email;
return keySynchronizer.synchronize(key, () -> {
SomeData[] existing = (SomeData[]) StaticCache.get(key);
if (existing == null) {
SomeData[] data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
return data;
}
logger.debug("getSomeDataForEmail: using cached object");
return existing;
});
}
如果没有应该从同步的代码返回,同步方法可以这样写:
public void synchronize(T key, Runnable runnable) {
CounterLock lock = locks.compute(key, (k, v) ->
v == null ? new CounterLock() : v.increment());
synchronized (lock) {
try {
runnable.run();
} finally {
if (lock.decrement() == 0) {
// Only removes if key still points to the same value,
// to avoid issue described below.
locks.remove(key, lock);
}
}
}
}
Answer 16:
我添加了一个小锁类,它可以锁定/上的任意键,包括字符串同步。
对Java 8,Java 6的和一个小测试,参见实施。
Java的8:
public class DynamicKeyLock<T> implements Lock
{
private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();
private final T key;
public DynamicKeyLock(T lockKey)
{
this.key = lockKey;
}
private static class LockAndCounter
{
private final Lock lock = new ReentrantLock();
private final AtomicInteger counter = new AtomicInteger(0);
}
private LockAndCounter getLock()
{
return locksMap.compute(key, (key, lockAndCounterInner) ->
{
if (lockAndCounterInner == null) {
lockAndCounterInner = new LockAndCounter();
}
lockAndCounterInner.counter.incrementAndGet();
return lockAndCounterInner;
});
}
private void cleanupLock(LockAndCounter lockAndCounterOuter)
{
if (lockAndCounterOuter.counter.decrementAndGet() == 0)
{
locksMap.compute(key, (key, lockAndCounterInner) ->
{
if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
return null;
}
return lockAndCounterInner;
});
}
}
@Override
public void lock()
{
LockAndCounter lockAndCounter = getLock();
lockAndCounter.lock.lock();
}
@Override
public void unlock()
{
LockAndCounter lockAndCounter = locksMap.get(key);
lockAndCounter.lock.unlock();
cleanupLock(lockAndCounter);
}
@Override
public void lockInterruptibly() throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
try
{
lockAndCounter.lock.lockInterruptibly();
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
}
@Override
public boolean tryLock()
{
LockAndCounter lockAndCounter = getLock();
boolean acquired = lockAndCounter.lock.tryLock();
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
boolean acquired;
try
{
acquired = lockAndCounter.lock.tryLock(time, unit);
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public Condition newCondition()
{
LockAndCounter lockAndCounter = locksMap.get(key);
return lockAndCounter.lock.newCondition();
}
}
Java 6中:
公共类DynamicKeyLock实现锁定{私人最终静态的ConcurrentHashMap locksMap =新的ConcurrentHashMap(); 私人最终T键;
public DynamicKeyLock(T lockKey) {
this.key = lockKey;
}
private static class LockAndCounter {
private final Lock lock = new ReentrantLock();
private final AtomicInteger counter = new AtomicInteger(0);
}
private LockAndCounter getLock()
{
while (true) // Try to init lock
{
LockAndCounter lockAndCounter = locksMap.get(key);
if (lockAndCounter == null)
{
LockAndCounter newLock = new LockAndCounter();
lockAndCounter = locksMap.putIfAbsent(key, newLock);
if (lockAndCounter == null)
{
lockAndCounter = newLock;
}
}
lockAndCounter.counter.incrementAndGet();
synchronized (lockAndCounter)
{
LockAndCounter lastLockAndCounter = locksMap.get(key);
if (lockAndCounter == lastLockAndCounter)
{
return lockAndCounter;
}
// else some other thread beat us to it, thus try again.
}
}
}
private void cleanupLock(LockAndCounter lockAndCounter)
{
if (lockAndCounter.counter.decrementAndGet() == 0)
{
synchronized (lockAndCounter)
{
if (lockAndCounter.counter.get() == 0)
{
locksMap.remove(key);
}
}
}
}
@Override
public void lock()
{
LockAndCounter lockAndCounter = getLock();
lockAndCounter.lock.lock();
}
@Override
public void unlock()
{
LockAndCounter lockAndCounter = locksMap.get(key);
lockAndCounter.lock.unlock();
cleanupLock(lockAndCounter);
}
@Override
public void lockInterruptibly() throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
try
{
lockAndCounter.lock.lockInterruptibly();
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
}
@Override
public boolean tryLock()
{
LockAndCounter lockAndCounter = getLock();
boolean acquired = lockAndCounter.lock.tryLock();
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
LockAndCounter lockAndCounter = getLock();
boolean acquired;
try
{
acquired = lockAndCounter.lock.tryLock(time, unit);
}
catch (InterruptedException e)
{
cleanupLock(lockAndCounter);
throw e;
}
if (!acquired)
{
cleanupLock(lockAndCounter);
}
return acquired;
}
@Override
public Condition newCondition()
{
LockAndCounter lockAndCounter = locksMap.get(key);
return lockAndCounter.lock.newCondition();
}
}
测试:
public class DynamicKeyLockTest
{
@Test
public void testDifferentKeysDontLock() throws InterruptedException
{
DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
lock.lock();
AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
try
{
new Thread(() ->
{
DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
anotherLock.lock();
try
{
anotherThreadWasExecuted.set(true);
}
finally
{
anotherLock.unlock();
}
}).start();
Thread.sleep(100);
}
finally
{
Assert.assertTrue(anotherThreadWasExecuted.get());
lock.unlock();
}
}
@Test
public void testSameKeysLock() throws InterruptedException
{
Object key = new Object();
DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
lock.lock();
AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
try
{
new Thread(() ->
{
DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
anotherLock.lock();
try
{
anotherThreadWasExecuted.set(true);
}
finally
{
anotherLock.unlock();
}
}).start();
Thread.sleep(100);
}
finally
{
Assert.assertFalse(anotherThreadWasExecuted.get());
lock.unlock();
}
}
}
Answer 17:
您可以安全地使用为的String.intern同步,如果你能合理地保证该字符串值是整个系统中是唯一的。 的UUID是解决这个的好方法。 您可以通过缓存,地图的UUID与实际的字符串键关联,要么,或者甚至在UUID存储为你的实体对象的字段。
@Service
public class MySyncService{
public Map<String, String> lockMap=new HashMap<String, String>();
public void syncMethod(String email) {
String lock = lockMap.get(email);
if(lock==null) {
lock = UUID.randomUUID().toString();
lockMap.put(email, lock);
}
synchronized(lock.intern()) {
//do your sync code here
}
}
文章来源: Synchronizing on String objects in Java