在工作面试中有人问我一个并发性问题,最终将其归结为以下要求。仅通过使用互斥,我就可以实现#2至#4,但不能实现#1。
使用以下方法设计任务队列:
public void registerCallback(Runnable task)
public void eventFired()
- 多个线程应该能够将任务放在队列中,并发。
eventFired
只能被调用一次。- 如果先前已调用
eventFired
,则以后对这两个方法的任何调用都将引发异常。- 如果在执行
eventFired
时调用了registerCallback
,请延迟触发事件,直到以后。- 如果
registerCallback
执行期间调用eventFired
,则引发异常。
ReentrantReadWriteLock
似乎很有希望,因为registerCallback
可以获取读锁定,而eventFired
可以获取写锁定,但这不能解决调用registerCallback
的竞争条件,然后是eventFired
。
有什么想法吗?
答案 0 :(得分:0)
ReentrantReadWriteLock
似乎很有希望,因为registerCallback
可以获取读锁定,而eventFired
可以获取写锁定,但这不能解决调用registerCallback
的竞争条件,然后是eventFired
。
注册回调,换句话说,在保持读取锁定的同时修改数据结构绝不是一个好主意。您需要另一个线程安全的构造来存储回调,并使代码不必要地复杂化。
注册回调的操作(例如将引用存储到某个集合中或实例化任何类型的节点对象)的琐碎操作足以允许使用普通的互斥锁,因为该锁持有时间不长。
无论您使用synchronized
,Lock
还是支持原子更新的状态,在两种情况下都不存在竞争条件,因为{{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>