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

时间:2015-12-22 13:58:40

标签: java-ee ejb ejb-3.1

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

  • 我将方法标记为@Asynchronous,因此它被执行,因此其他客户端仍然可以在不同的执行线程中同时访问getter。
  • 我将其标记为LockType.Write并使用AccessTimeout,因为我想在短时间内删除冗余事件。

问题很明显:

  • 如果我使用LockType.WRITE,则在重新计算期间也会锁定对getter方法的访问权。
  • 如果我没有使用它,我会在观察到许多CDI事件时最终耗费大量资源消耗

是否可以仅锁定对此单一方法的访问权限,而不是锁定对我的单例的整个实例的访问权限?这会解决我的问题吗?我可以用其他方法处理我的问题吗?

如何处理?

2 个答案:

答案 0 :(得分:1)

选项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()。这可能会发生,但我认为这是非常罕见的,不等待所有大多数时间的优势超过等待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)注释并以您希望的方式锁定对方法的访问权。

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