同步上串的对象,在Java
-
02-07-2019 - |
题
我有一个网络应用程序,我的中间做一些装载/性能测试,特别是在一个特征,我们期望几百个用户可以访问同一页和打刷新关于每10秒钟在这个网页。一个改进的领域,我们发现,我们可以使用这一功能是缓的反应网络服务一段时间,由于数据没有变化。
后执行这一基本缓存,在一些进一步的检测我发现,我没有考虑如何并发线程可以访问缓存在同样的时间。我发现在的问题~100毫秒,约50线试图获取的对象从高速缓存,找到它已经到期,击中网络的服务来获取数据,然后把对象回来的高速缓存。
原始代码看起来是这样的:
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
过期的,我认为我需要同步的高速缓存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''记录器名称''message')
http-80-Processor253jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor253jsp。看页-getSomeDataForEmail:内部同步块
http-80-Processor253缓存。StaticCache-获得:目在关键[SomeData-test@test.com]已经到期
http-80-Processor253缓存。StaticCache-获得:关键[SomeData-test@test.com]回价值的[空]
http-80-Processor263jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor263jsp。看页-getSomeDataForEmail:内部同步块
http-80-Processor263缓存。StaticCache-获得:目在关键[SomeData-test@test.com]已经到期
http-80-Processor263缓存。StaticCache-获得:关键[SomeData-test@test.com]回价值的[空]
http-80-Processor131jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor131jsp。看页-getSomeDataForEmail:内部同步块
http-80-Processor131缓存。StaticCache-获得:目在关键[SomeData-test@test.com]已经到期
http-80-Processor131缓存。StaticCache-获得:关键[SomeData-test@test.com]回价值的[空]
http-80-Processor104jsp。看页-getSomeDataForEmail:内部同步块
http-80-Processor104缓存。StaticCache-获得:目在关键[SomeData-test@test.com]已经到期
http-80-Processor104缓存。StaticCache-获得:关键[SomeData-test@test.com]回价值的[空]
http-80-Processor252jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor283jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor2jsp。看页-getSomeDataForEmail:关于进入同步块
http-80-Processor2jsp。看页-getSomeDataForEmail:内部同步块
我想看看只有一个线程的时间进入/退出的同步框周围get/set行动。
是有一个问题同步上串的对象?我以为缓存的关键将是一个好的选择,因为它是独有的操作,而且即使 final String key
宣告内所述的方法,我想,每个线程将得到一个参考 同样的对象 因此会上同步,这个单一的对象。
我做错了这里?
更新:之后的寻找还在日志,这似乎是方法一样同步的逻辑哪里的关键是总是相同的,例如
final String key = "blah";
...
synchronized(key) { ...
没有表现出同样的并发问题的唯一线在一个时间进入区块。
更新2:感谢大家的帮助!我接受了这一答案有关 intern()
ing串的,这解决了我最初的问题-其中多线程正在进入同步块在哪里我以为他们不应该因为 key
's有同样的价值。
正如其他人已经指出,使用 intern()
为此目的同步上那些字符串的确是个坏主意-时运行创建属测试对该网络应用程序模拟的预期负荷,我看到了使用堆尺寸增大到几乎1GB在仅仅20分钟。
目前我使用的是简单的解决方案的只是同步的整个方法-但是我 真的 喜欢的代码样本提供的martinprobst和MBCook,但由于我们有关7类似的 getData()
方法在这类目前(由于它所需要的关于7个不同的数据从一个网页服务),我不想加入几乎重复的逻辑关于获得和释放锁的每一个方法。但这绝对是非常,非常有价值的信息,为未来的使用情况。我认为这些最终都是正确的答案如何最好做出一个操作的喜欢这个线安全,并且我得到更多选民到这些问题的答案如果我可以!
解决方案
没有把我的大脑完成的齿轮,从一个"快速扫描"为什么你说这看起来好像你需要来实习()你Strings:
final String firstkey = "Data-" + email;
final String key = firstkey.intern();
两个字相同的价值是无不一定是相同的对象。
注意,这可能会引入一个新的争论的焦点,因为在深VM,实习()可能需要获得锁。我不知道有什么现代化的虚拟机看起来像在这个区域,但人们希望,他们被恶魔般的优化。
我假设你知道,StaticCache仍然需要线的安全。但是,竞争应该有微小的比什么你如果你被锁定在高速缓存,而不仅仅是关键,同时呼吁getSomeDataForEmail.
响应问题的更新:
我认为这是因为一个字符串总是会产生相同的对象。戴维斯指出,在评论,它甚至比这更好的:字面始终产生的规范的表示。因此,所有的字符串同价值的任何地方在该计划将产生相同的对象。
编辑
其他人已经指出, 同步实习生串实际上是一个非常糟糕的主意 -部分原因是因为创造实习生串被许可导致他们的存在永久化,部分原因是因为如果多于一位代码的任何地方在你的程序同步实习生弦,你们之间的依赖关系的那些代码位,并防止僵局或其他错误可能是不可能的。
战略,以避免这种储存的锁定对象的每个关键字是正在开发其他的答案为我的类型。
这是一个选择-它仍然使用一种奇异的锁,但我们知道我们需要那些对缓存,无论如何,你在谈论约50线,不5000,因此,可能不是致命的。我也是假设业绩的瓶颈,这里是缓慢阻止I/O DoSlowThing()这将会因此大大受益于不正序列化.如果那不是瓶颈,则:
- 如果CPU是忙碌的那么这种方法可能不足以及需要另一个方法。
- 如果CPU是不忙,并访问服务器是不是一个瓶颈,那么这种方法被矫枉过正,并且你可以忘记这个和每钥匙锁,把一个很大的同步(StaticCache)周围的整个操作,并做到简单的方法。
显然这种方法需要测试浸泡为扩展性的前使用的-我保证什么都没有。
这种代码并不要求StaticCache是同步的,或是无线的安全。需要重新审查,如果任何其他代码(例如计划的清理旧的数据)没有触及的高速缓存。
IN_PROGRESS是一个虚拟值-不完全是干净的,但是代码是简单的,它节省了具有两个hashtables.它不处理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,所以它不可能是一个问题。
这个代码可以被commoned-为使用有多个缓存,如果定义适合的抽象的高速缓存及其相关锁,数据的返回,IN_PROGRESS虚设,以及缓慢的操作,以执行。滚动整个事情变成一个方法对缓可能不是一个坏主意。
其他提示
对intern'd String进行同步可能根本不是一个好主意 - 通过实习,String变成一个全局对象,如果你在应用程序的不同部分同步interned字符串,你可能会得到真的很奇怪,基本上是不可解决的同步问题,比如死锁。这似乎不太可能,但是当它发生时,你真的被搞砸了。作为一般规则,只有在本地对象上进行同步才能确保模块外部的代码不会锁定它。
在您的情况下,您可以使用同步哈希表来存储密钥的锁定对象。
E.g:
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
}
此代码具有竞争条件,其中两个线程可能会将对象彼此放入锁定表中。这应该不是问题,因为那时你只有一个线程调用webservice并更新缓存,这应该不是问题。
如果您在一段时间后使缓存失效,则应在从锁定!= null情况下从缓存中检索数据后再检查数据是否为空。
或者,更容易,您可以使整个缓存查找方法(“getSomeDataByEmail”)同步。这意味着所有线程在访问缓存时都必须进行同步,这可能是性能问题。但是一如既往,首先尝试这个简单的解决方案,看看它是否真的是一个问题!在许多情况下它不应该是,因为你可能花费更多的时间处理结果而不是同步。
字符串 不 好的候选人进行同步。如果你必须同步串上的ID,它可以通过使用串创建一个互斥(见"同步,在一个ID").是否有成本的,算法是值得取决于是否援引你的服务涉及任何明显的I/O.
也:
- 我希望的 StaticCache.获得() 和 设置() 方法都是线程安全的。
- String.实习生() 来在费用(一个变化之间的VM实现)和应小心使用。
其他人建议实习字符串,这将有效。
问题是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个)线程需要尝试创建或只是等待结果显示在缓存中。我想我有三个建议。
您可以使用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类的基础。
使用合适的缓存框架,例如 ehcache 。
实现好的缓存并不像有些人认为的那么容易。
关于String.intern()是内存泄漏源的注释,实际上并非如此。 Interned Strings 垃圾收集,它可能需要更长时间,因为在某些JVM'S(SUN)上它们存储在Perm空间中,只有完整的GC触及它。
这是一个安全的短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
是线程安全的,不会受到不同密钥的并发读写操作的影响。
您的主要问题不仅仅是可能存在多个具有相同值的String实例。主要问题是您只需要一个用于同步的监视器来访问StaticCache对象。否则,多个线程可能最终同时修改StaticCache(尽管在不同的密钥下),这很可能不支持并发修改。
电话:
final String key = "Data-" + email;
每次调用方法时,都会创建一个新对象。因为该对象是您用来锁定的对象,并且每次调用此方法都会创建一个新对象,所以您实际上并不是基于该键同步对地图的访问。
这进一步说明了您的编辑。如果你有一个静态字符串,那么它将起作用。
使用intern()解决了这个问题,因为它从String类保存的内部池中返回字符串,这确保了如果两个字符串相等,则将使用池中的字符串。参见
http:/ /java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()
这个问题在我看来有点过于宽泛,因此它煽动了同样广泛的答案。所以我将尝试回答问题我被重定向,不幸的是,一个已经被关闭了。
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);
}
这已经相当晚了,但这里提供了很多不正确的代码。
在这个例子中:
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 / put API的静态缓存,至少应该围绕get和getIfAbsentPut类型操作进行同步,以便安全访问缓存。同步范围将是缓存本身。
如果必须对数据元素本身进行更新,则会增加一个额外的同步层,该层应该在各个数据元素上。
可以使用SynchronizedMap代替显式同步,但仍必须注意。如果使用了错误的API(get和put而不是putIfAbsent),那么尽管使用了synchronized映射,但操作将没有必要的同步。注意使用putIfAbsent引入的复杂性:要么,即使在不需要的情况下也必须计算put值(因为put不知道在检查缓存内容之前是否需要put值),或者需要小心使用委托(例如,使用Future,它有效,但有些不匹配;见下文),如果需要,可以根据需要获得put值。
使用期货是可能的,但似乎相当尴尬,也许有点过度工程。 Future API是异步操作的核心,特别是对于可能无法立即完成的操作。涉及未来很可能会增加一层线程创建 - 额外可能是不必要的复杂性。
将Future用于此类操作的主要问题是Future在多线程中具有内在联系。在没有必要使用新线程时使用Future意味着忽略了Future的许多机制,使其成为过度使用的API。
为什么不渲染一个静态的html页面,该页面被提供给用户并每x分钟重新生成一次?
如果你不需要,我还建议完全摆脱字符串连接。
final String key = "Data-" + email;
缓存中是否有其他事物/类型的对象使用您需要的额外“数据 - ”的电子邮件地址。在钥匙的开头?
如果没有,我只是做那个
final String key = email;
并且你也避免了所有额外的字符串创建。
以其他方式同步字符串对象:
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
}
}
}
如果其他人遇到类似的问题,就我所知,以下代码有效:
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);
}
}
}
}
我添加了一个小锁类,可以锁定/同步任何键,包括字符串。
请参阅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实现Lock { private final static ConcurrentHashMap locksMap = new 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();
}
}
}
在你的情况下你可以使用这样的东西(这不会泄漏任何内存):
private Synchronizer<String> synchronizer = new Synchronizer();
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
String key = "Data-" + email;
return synchronizer.synchronizeOn(key, () -> {
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;
});
}
使用它只需添加依赖项:
compile 'com.github.matejtymes:javafixes:1.3.0'
如果可以合理地保证字符串值在整个系统中是唯一的,则可以安全地使用String.intern进行同步。 UUIDS是解决这个问题的好方法。您可以通过缓存,地图将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
}
}