如何设计具有互斥但独立的并发方法的任务队列?

时间:2019-06-08 22:40:40

标签: java multithreading concurrency java.util.concurrent

在工作面试中有人问我一个并发性问题,最终将其归结为以下要求。仅通过使用互斥,我就可以实现#2至#4,但不能实现#1。

  

使用以下方法设计任务队列:

     

public void registerCallback(Runnable task)

     

public void eventFired()

     
      
  1. 多个线程应该能够将任务放在队列中,并发。
  2.   
  3. eventFired只能被调用一次。
  4.   
  5. 如果先前已调用eventFired,则以后对这两个方法的任何调用都将引发异常。
  6.   
  7. 如果在执行eventFired时调用了registerCallback,请延迟触发事件,直到以后。
  8.   
  9. 如果registerCallback执行期间调用eventFired,则引发异常。
  10.   

ReentrantReadWriteLock似乎很有希望,因为registerCallback可以获取读锁定,而eventFired可以获取写锁定,但这不能解决调用registerCallback的竞争条件,然后是eventFired

有什么想法吗?

1 个答案:

答案 0 :(得分:0)

  

ReentrantReadWriteLock似乎很有希望,因为registerCallback可以获取读锁定,而eventFired可以获取写锁定,但这不能解决调用registerCallback的竞争条件,然后是eventFired

注册回调,换句话说,在保持读取锁定的同时修改数据结构绝不是一个好主意。您需要另一个线程安全的构造来存储回调,并使代码不必要地复杂化。

注册回调的操作(例如将引用存储到某个集合中或实例化任何类型的节点对象)的琐碎操作足以允许使用普通的互斥锁,因为该锁持有时间不长。

无论您使用synchronizedLock还是支持原子更新的状态,在两种情况下都不存在竞争条件,因为{{1}不能重叠}或registerCallback。当正确使用时,所有这些方法都会使这些操作有序。因此,每个eventFired都在第一个registerCallback之前或之后。

实现这一点很简单:

eventFired

public class JobQueue { private List<Runnable> callbacks = new ArrayList<>(); public synchronized void registerCallback(Runnable task) { if(callbacks == null) { throw new IllegalStateException("Event already fired"); } callbacks.add(Objects.requireNonNull(task)); } public void eventFired() { List<Runnable> list; synchronized(this) { list = callbacks; callbacks = null; } if(list == null) { throw new IllegalStateException("Can only fire once"); } for(Runnable r: list) r.run(); } } 块中执行的代码很短,因此对于大多数实际用例而言,争用是无关紧要的。用synchronized实现相同的操作很简单,但是没有任何好处。实际上,JVM特定的优化可以使基于Lock的解决方案更加有效。

出于完整性考虑,这里是基于原子更新的解决方案:

synchronized

实际上,多个public class JobQueue { private static final Runnable DONE = () -> {}; private final AtomicReference<Runnable> pending = new AtomicReference<>(); public void registerCallback(Runnable task) { Objects.requireNonNull(task); for(;;) { Runnable previous = pending.get(); if(previous == DONE) throw new IllegalStateException("Event already fired"); if(pending.compareAndSet(previous, sequence(previous, task))) return; } } public void eventFired() { Runnable previous = pending.getAndSet(DONE); if(previous == DONE) throw new IllegalStateException("Can only fire once"); if(previous != null) previous.run(); } static Runnable sequence(Runnable a, Runnable b) { return a == null? b: () -> { a.run(); b.run(); }; } } 和/或registerCallback调用的执行可能会重叠,但是在那种情况下,只有一个可以成功执行关键原子更新。这使操作进入一个顺序,使最多一个eventFired调用成功,并将所有eventFired调用归类为在之前之后。 / p>