考虑从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。 在同一线程的run方法中,
消耗EventSource线程不断将对象从阻塞队列中取出并处理它们。如果同一个线程试图从空队列中取出一个对象,则阻塞同一个线程,直到producing thread
(主线程)将一个对象放入队列。
由于这两个操作(下面)不是原子的,由于一些不幸的timimg,所以在相同的2个操作之间,很可能EventSource可能会发现 num!= 2 &amp;因此竞争条件。
source.registerListener(new EventListener() { // OPERATION 1
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42; // OPERATION 2
}
由于建议和清楚地看到,内部类实例包含对封闭实例的隐藏引用。
虽然锁已被同一个线程(主线程)获取,但非同步方法doSomething()
仍然可以被另一个线程(在本例中为EventSource)访问同时,我看到即使同步上述两项操作也无法避免竞争条件。我的理解是否正确?
我的意思是
public ThisEscape(EventSource source) {
synchronized(this){ // SYNCHRONISED
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
}
避免竞争条件的唯一方法是使doSomething()
方法同步,除了同步2个操作?
第三,我看到该领域是否是最终的,它没有任何区别。竞争条件仍将存在。作者关于最终字段的讨论究竟是什么(除了制作private final int num = 42
)?
答案 0 :(得分:4)
正如您已经意识到的那样,在构造函数中发布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;
}
答案 1 :(得分:1)
通过在初始化registerListener()
字段之前调用num
,您显然会面临在设置之前访问num的风险。更重要的是,num
是从不同的线程访问的,因此无法保证一旦设置,就会读取正确的值。
可能的解决方案是事先初始化num
public static class ThisEscape {
private final int num = 42;
public ThisEscape(EventSource source) {
source.registerListener(e -> doSomething(e));
}
//...
}
或者让它变得易变并在调用registerListener()
之前设置它
public static class ThisEscape {
private volatile int num;
public ThisEscape(EventSource source) {
num = 42;
source.registerListener(e -> doSomething(e));
}
//...
}
编辑:感谢@AndyTurner和@ShirgillFarhanAnsari指出错误。
答案 2 :(得分:0)
这与转义this
指针无关。 num
在外部类中的事实并没有改变并行活动需要同步的事实。
如您所述,有两项操作:
registerListener
num = 42
现在onEvent
回调可以在registerListener
返回后不久或甚至在它返回之前调用,因为它是异步的。在任何情况下 之前或 num = 42
之后。它需要同步或正确订购。