在我的应用程序中,我在@Singleton
EJB中提供了一些应用程序范围的数据。数据的预制是一项长期运行的任务。然而,这种结果的根本来源经常发生变化,所以我不得不重新计算Singleton经常填充的数据。
现在的任务是确保快速访问数据,即使它当前已重新计算。在准备工作正在进行时,如果我暴露旧状态并不重要。
我目前的做法如下:
@Singleton
@Startup
@Lock(LockType.READ)
public class MySingleton {
@Inject private SomeService service;
@Getter private SomeData currentVersion;
@PostConstruct
private void init() {
update();
}
private void update() {
currentVersion = service.longRunningTask();
}
@Lock(LockType.WRITE)
@AccessTimeout(value = 1, unit = TimeUnit.MINUTES)
@Asynchronous
public void catchEvent(
@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event) {
this.update();
}
}
这背后的想法是在启动时准备一次数据。之后,客户端可以同时访问我的数据的当前版本(将此数据视为不可变)。
现在,如果发生可能导致重新计算数据的某些操作,我正在触发CDI事件,而在Singleton中,我正在观察此事件并再次触发重新计算。这些行为可能会在很短的时间内发生(但也可能长时间没有这些行动)。
问题很明显:
LockType.WRITE
,则在重新计算期间也会锁定对getter方法的访问权。是否可以仅锁定对此单一方法的访问权限,而不是锁定对我的单例的整个实例的访问权限?这会解决我的问题吗?我可以用其他方法处理我的问题吗?
如何处理?
答案 0 :(得分:1)
选项A:从不等待超过1个线程:
@Singleton
@Startup
@Lock(LockType.READ)
public class MySingleton {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// ...
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
try{
if(!lock.writeLock().tryLock()){
// Unable to immediately obtain lock
if(lock.hasQueuedThreads()){
// there is *probably* at least 1 thread waiting on
// the lock already, don't have more than 1 thread waiting
return;
} else {
// There are no other threads waiting. Wait to acquire lock
lock.writeLock().lock();
this.update();
}
} else {
// obtained the lock on first try
this.update();
}
} finally {
try {
lock.writeLock().unlock();
} catch (IllegalMonitorStateException ignore){}
}
}
}
注意: Brett Kail指出,这里可能存在竞争条件,其中两个线程同时通过tryLock()
和lock.hasQueuedThreads()
,其中一个获得写锁定,而其他阻塞整个持续时间为this.update()
。这可能会发生,但我认为这是非常罕见的,不等待所有大多数时间的优势超过等待1分钟大多数的时间。
选项B:永远不要等待任何线程:
我不确定你的事件是如何稳定进来的,但是如果你在tryLock()
返回false时中止了它会更多地简化逻辑,但是你不会得到更多的更新。
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
if(!lock.writeLock().tryLock())
return; // there is already an update processing
try{
this.update();
} finally {
lock.writeLock().unlock();
}
}
选项C:等待最多X次锁定(模拟@AccessTimeout)
Brett在评论中指出,选项A具有竞争条件,并且可能在this.update()
的整个持续时间内有一个线程阻塞,并建议将此方法作为更安全的替代方案。唯一的缺点是tryLock()
将在大多数时间结束超时,因此将超时设置为尽可能小是有利的。
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
if(lock.writeLock().tryLock(1, TimeUnit.MINUTES)) {
try{
this.update();
} finally {
lock.writeLock().unlock();
}
}
}
答案 1 :(得分:1)
我宁愿不鼓励你将容器管理的并发与手动同步混合在一起。我相信最好使用bean管理的并发和JavaSE同步技术来更好地控制正在发生的事情。只需使用@ConcurrencyManagement(BEAN)注释并以您希望的方式锁定对方法的访问权。
您也可以尝试继续使用容器管理的并发,并将逻辑拆分为两个单独的单例,首先是数据读取,第二个是长时间运行的东西。第二个可以在收集结果时为第一个提供结果,使用快速锁定方法,仅用于使用新的计算部分数据替换旧数据。