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

时间:2016-08-05 11:51:43

标签: java race-condition

考虑从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)?

3 个答案:

答案 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在外部类中的事实并没有改变并行活动需要同步的事实。

如您所述,有两项操作:

  1. registerListener
  2. 设置num = 42
  3. 现在onEvent回调可以在registerListener返回后不久或甚至在它返回之前调用,因为它是异步的。在任何情况下 之前 num = 42之后。它需要同步或正确订购。