【问题标题】:EJB: Lock access to a method only instead of whole bean instanceEJB:仅锁定对方法的访问,而不是整个 bean 实例
【发布时间】:2015-12-22 13:58:40
【问题描述】:

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

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

问题很明显:

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

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

如何处理?

【问题讨论】:

    标签: jakarta-ee ejb ejb-3.1


    【解决方案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() 的整个持续时间内的其他块。这可能会发生,但我认为这种情况非常罕见,以至于 most 时间根本不等待的优势超过了 most 时间等待 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 在 cmets 中指出,选项 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();
            }
        }
    }     
    

    【讨论】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2011-12-08
      • 1970-01-01
      • 2012-05-19
      • 1970-01-01
      • 2012-08-23
      • 1970-01-01
      • 1970-01-01
      • 2014-01-14
      • 2014-10-20
      相关资源
      最近更新 更多