如何避免在以下代码中检测到竞争条件?

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 实例包含对封闭实例的隐藏引用。

虽然锁已经被同一个线程([=3​​7=]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 个操作:

  1. registerListener
  2. 设置num = 42

现在 onEvent 回调可以在 registerListener returns 之后不久甚至在 returns 之前调用,因为它是异步的。在任何情况下 beforeafter 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 指出错误。