如何避免在以下代码中检测到竞争条件?
How to avoid the race condition detected in the following code?
考虑摘自 here 的片段:
// event
public class Event { }
// An Event Listener
public interface EventListener {
public void onEvent(Event e);
}
// inner class instances contain a hidden reference to the enclosing instance
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " + new Date());
}
}
}
// event source
public class EventSource extends Thread {
private final BlockingQueue<EventListener> listeners =
new LinkedBlockingQueue<EventListener>();
public void run() {
while (true) {
try {
listeners.take().onEvent(null);
} catch (InterruptedException e) {
break;
}
}
}
public void registerListener(EventListener eventListener) {
listeners.add(eventListener);
}
}
// testing the conditions
public class ThisEscapeTest {
public static void main(String[] args) {
EventSource es = new EventSource();
es.start();
while (true) {
new ThisEscape(es);
}
}
}
为了整合,我们有 2 个线程
// Main Thread
// Event Source
在Event Source线程中,有一个BlockingQueue用来存放EventListener。
在同一线程的 运行 方法中,
消费 EventSource 线程不断从阻塞队列中取出对象,并处理它们。如果同一个线程试图从空队列中取出一个对象,则同一个线程将被阻塞,直到 producing thread
( 主线程)将一个对象放入队列。
由于这 2 个操作(下面)不是原子的,由于一些不幸的时间,所以在相同的 2 个操作之间,EventSource 很可能会发现 num != 2 & 因此竞争条件。
source.registerListener(new EventListener() { // OPERATION 1
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42; // OPERATION 2
}
因为正如建议的那样并且看得很清楚,内部 class 实例包含对封闭实例的隐藏引用。
虽然锁已经被同一个线程([=37=]Main Thread)获取了,但是非同步方法doSomething()
仍然可以被另一个线程访问(在此case EventSource),我发现即使同步上面的两个操作也无法避免竞争条件。我的理解正确吗?
我是说
public ThisEscape(EventSource source) {
synchronized(this){ // SYNCHRONISED
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
}
避免竞争条件的唯一方法是使 doSomething()
方法也同步,除了同步 2 个操作?
第三,我看这个字段是不是final,这没什么区别。竞争条件仍然存在。作者关于 final 字段(除了制作 private final int num = 42
)的讨论要点到底是什么?
正如您已经意识到的那样,在构造函数中发布 this
是一个非常糟糕的主意。
这是一个很好的解决方法;使用工厂方法。
public static ThisEscape newInstance(EventSource source){
final ThisEscape instance = new ThisEscape();
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
instance.doSomething(e);
}
}
return instance;
}
这与转义 this
指针无关。 num
在外部 class 的事实并没有改变并行活动需要同步的事实。
如您所述,有 2 个操作:
registerListener
- 设置
num = 42
现在 onEvent
回调可以在 registerListener
returns 之后不久甚至在 returns 之前调用,因为它是异步的。在任何情况下 before 或 after num = 42
。需要同步或正确排序。
通过在初始化 num
字段之前调用 registerListener()
,您显然将自己置于 num 在设置之前被访问的风险之中。更重要的是,num
是从不同的线程访问的,因此不能保证一旦设置,就会读取正确的值。
可能的解决方案是预先初始化 num
public static class ThisEscape {
private final int num = 42;
public ThisEscape(EventSource source) {
source.registerListener(e -> doSomething(e));
}
//...
}
或者在 registerListener()
被调用之前设置它为 volatile
public static class ThisEscape {
private volatile int num;
public ThisEscape(EventSource source) {
num = 42;
source.registerListener(e -> doSomething(e));
}
//...
}
编辑:感谢@AndyTurner 和@ShirgillFarhanAnsari 指出错误。
考虑摘自 here 的片段:
// event
public class Event { }
// An Event Listener
public interface EventListener {
public void onEvent(Event e);
}
// inner class instances contain a hidden reference to the enclosing instance
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " + new Date());
}
}
}
// event source
public class EventSource extends Thread {
private final BlockingQueue<EventListener> listeners =
new LinkedBlockingQueue<EventListener>();
public void run() {
while (true) {
try {
listeners.take().onEvent(null);
} catch (InterruptedException e) {
break;
}
}
}
public void registerListener(EventListener eventListener) {
listeners.add(eventListener);
}
}
// testing the conditions
public class ThisEscapeTest {
public static void main(String[] args) {
EventSource es = new EventSource();
es.start();
while (true) {
new ThisEscape(es);
}
}
}
为了整合,我们有 2 个线程
// Main Thread
// Event Source
在Event Source线程中,有一个BlockingQueue用来存放EventListener。 在同一线程的 运行 方法中,
消费 EventSource 线程不断从阻塞队列中取出对象,并处理它们。如果同一个线程试图从空队列中取出一个对象,则同一个线程将被阻塞,直到 producing thread
( 主线程)将一个对象放入队列。
由于这 2 个操作(下面)不是原子的,由于一些不幸的时间,所以在相同的 2 个操作之间,EventSource 很可能会发现 num != 2 & 因此竞争条件。
source.registerListener(new EventListener() { // OPERATION 1
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42; // OPERATION 2
}
因为正如建议的那样并且看得很清楚,内部 class 实例包含对封闭实例的隐藏引用。
虽然锁已经被同一个线程([=37=]Main Thread)获取了,但是非同步方法doSomething()
仍然可以被另一个线程访问(在此case EventSource),我发现即使同步上面的两个操作也无法避免竞争条件。我的理解正确吗?
我是说
public ThisEscape(EventSource source) {
synchronized(this){ // SYNCHRONISED
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
}
避免竞争条件的唯一方法是使 doSomething()
方法也同步,除了同步 2 个操作?
第三,我看这个字段是不是final,这没什么区别。竞争条件仍然存在。作者关于 final 字段(除了制作 private final int num = 42
)的讨论要点到底是什么?
正如您已经意识到的那样,在构造函数中发布 this
是一个非常糟糕的主意。
这是一个很好的解决方法;使用工厂方法。
public static ThisEscape newInstance(EventSource source){
final ThisEscape instance = new ThisEscape();
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
instance.doSomething(e);
}
}
return instance;
}
这与转义 this
指针无关。 num
在外部 class 的事实并没有改变并行活动需要同步的事实。
如您所述,有 2 个操作:
registerListener
- 设置
num = 42
现在 onEvent
回调可以在 registerListener
returns 之后不久甚至在 returns 之前调用,因为它是异步的。在任何情况下 before 或 after num = 42
。需要同步或正确排序。
通过在初始化 num
字段之前调用 registerListener()
,您显然将自己置于 num 在设置之前被访问的风险之中。更重要的是,num
是从不同的线程访问的,因此不能保证一旦设置,就会读取正确的值。
可能的解决方案是预先初始化 num
public static class ThisEscape {
private final int num = 42;
public ThisEscape(EventSource source) {
source.registerListener(e -> doSomething(e));
}
//...
}
或者在 registerListener()
被调用之前设置它为 volatile
public static class ThisEscape {
private volatile int num;
public ThisEscape(EventSource source) {
num = 42;
source.registerListener(e -> doSomething(e));
}
//...
}
编辑:感谢@AndyTurner 和@ShirgillFarhanAnsari 指出错误。