Jakarta ee EJB:只锁定对方法的访问,而不是对整个bean实例的访问

Jakarta ee EJB:只锁定对方法的访问,而不是对整个bean实例的访问,jakarta-ee,ejb,ejb-3.1,Jakarta Ee,Ejb,Ejb 3.1,在我的应用程序中,我在@SingletonEJB中提供了一些应用程序范围的数据。数据的预处理是一项长期运行的任务。然而,这个结果的潜在来源经常变化,所以我不得不重新计算数据,这些数据也经常由单例填充 现在的任务是确保快速访问数据,即使当前正在重新计算数据。当准备工作正在进行时,我是否暴露旧的状态并不重要 我目前的做法如下: @Singleton @Startup @Lock(LockType.READ) public class MySingleton { @Inject privat

在我的应用程序中,我在
@Singleton
EJB中提供了一些应用程序范围的数据。数据的预处理是一项长期运行的任务。然而,这个结果的潜在来源经常变化,所以我不得不重新计算数据,这些数据也经常由单例填充

现在的任务是确保快速访问数据,即使当前正在重新计算数据。当准备工作正在进行时,我是否暴露旧的状态并不重要

我目前的做法如下:

@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事件,在单例中,我将观察该事件并再次触发重新计算。这些行动可能在短时间内经常发生(但也可能在长时间内没有这些行动)

  • 我将该方法标记为@Asynchronous以便执行,因此其他客户端仍可以在不同的执行线程中并发访问getter
  • 我将其标记为LockType.Write和AccessTimeout,因为我想在短时间内发生冗余事件时删除它们
问题显而易见:

  • 如果我使用的是
    LockType.WRITE
    ,那么在重新计算期间,对getter方法的访问也会被锁定
  • 如果我不使用它,当观察到许多CDI事件时,我最终会消耗大量资源
是否可以只锁定对这个方法的访问,而不是锁定对我的singleton的整个实例的访问?这能解决我的问题吗?有没有其他方法可以解决我的问题


如何应对

选项A:等待的线程不得超过1个:

  • 尝试仅获取此方法的WriteLock。如果获得锁成功,则更新并解锁
  • 如果tryLock未成功,请检查是否已经有线程等待获取WriteLock。
    • 如果有一个线程已经在等待,请中止该方法,因为我们不希望两个线程同时等待WriteLock
    • 如果没有其他线程等待获得锁,请等待写回,然后更新

注意: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个锁时间(mimic@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();
        }
    }
}     

我不鼓励您将容器管理的并发与手动同步混合使用。我认为最好使用bean管理的并发和JavaSE同步技术来更好地控制正在发生的事情。只需使用@concurrentymanagement(BEAN)注释,并以您想要的方式锁定对方法的访问


您还可以尝试继续使用容器管理的并发,并将逻辑拆分为两个单独的单例,第一个用于数据读取,第二个用于长时间运行的内容。第二个线程可以在收集到它们的结果后向第一个线程提供结果,使用快速、锁定的方法,仅用于用新的、计算过的数据部分替换旧数据。

在这种情况下,我将得到一堆挂起的执行线程,所有线程都将逐个执行,不是吗?所以实际上EJB超时是没有意义的,对吗?我想避免这种情况,因为在很短的时间内可能会发生多起甚至数百起这样的事件。理想情况下,在重新计算完成时,我希望最多只有一个挂起的执行线程。也许我不明白,也许我误解了这个问题。您希望此方法只锁定此方法的其他访问,而不阻止包括getter在内的任何其他访问,对吗?是的,
@AccessTimeout
在这种情况下变得毫无意义。如果您想要定时等待,那么您可以使用您自己的ReentrantReadWriteLock(或其他)而不是synchronized关键字。(+1)现在看起来很有希望,tyvm!-稍后将测试并接受答案,目前正忙于圣诞节准备工作:-)@aguibert tryLock和lock之间存在一场竞赛,这可能会导致锁无限期地阻塞。相反,您希望
tryLock(long,TimeUnit)
执行定时等待,我怀疑几乎所有EJB容器都实现了
@AccessTimeout
@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();
        }
    }
}