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

EJB: Lock access to a method only instead of whole bean instance

在我的应用程序中,我在 @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 中我将观察此事件并再次触发重新计算。这些动作可能会在短时间内经常发生(但也可能 none 这些动作会持续很长时间)。

问题很明显:

是否可以只锁定对这个单一方法的访问,而不是锁定对我的单例的整个实例的访问?这会解决我的问题吗?还有其他方法可以解决我的问题吗?

如何处理?

选项 A:永远不会有超过 1 个线程在等待:

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

.

@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() 的整个持续时间内阻塞。这可能会发生,但我认为这种情况很少见,完全不等待 most 的时间的优势胜过等待 1 分钟 most 的时间时间。


选项 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 时间(mimmic @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 同步技术来更好地控制正在发生的事情。只需使用 @ConcurrencyManagement(BEAN) 注释并以您想要的方式锁定对您的方法的访问。

您也可以尝试继续使用容器管理的并发并将您的逻辑拆分为两个单独的单例,第一个用于数据读取,第二个用于长 运行 东西。第二个可以在收集结果后为第一个提供结果,使用快速、锁定的方法只用新的、计算出的数据部分替换旧数据。