How does `this` reference to an outer class escape through publishing inner class instance?

前端 未结 3 1006
心在旅途
心在旅途 2020-11-28 06:36

This was asked slightly differently earlier but asking for a yes/no answer but I\'m looking for the explanation that\'s missing from the book (Java Concurrency in Practice),

相关标签:
3条回答
  • 2020-11-28 07:16

    Please see this article. There it's clearly explained what could happen when you let this escape.

    And here is a follow-up with further explanations.

    It's Heinz Kabutz amazing newsletter, where this and other very interesting topics are discussed. I highly recommend it.

    Here is the sample taken from the links, which show how the this reference escapes:

    public class ThisEscape {
      private final int num;
    
      public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
              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());
        }
      }
    }
    

    When it gets compiled, javac generates two classes. The outer class looks like this:

    public class ThisEscape {
      private final int num;
    
      public ThisEscape(EventSource source) {
        source.registerListener(new ThisEscape$1(this));
        num = 42;
      }
    
      private void doSomething(Event e) {
        if (num != 42)
          System.out.println(
              "Race condition detected at " + new Date());
      }
    
      static void access$000(ThisEscape _this, Event event) {
        _this.doSomething(event);
      }
    }
    

    Next we have the anonymous inner class:

    class ThisEscape$1 implements EventListener {
      final ThisEscape this$0;
    
      ThisEscape$1(ThisEscape thisescape) {
        this$0 = thisescape;
        super();
      }
    
      public void onEvent(Event e) {
        ThisEscape.access$000(this$0, e);
      }
    }
    

    Here the anonymous inner class created in the constructor of the outer class is converted to a package-access class that receives a reference to the outer class (the one that is allowing this to escape). For the inner class to have access to the attributes and methods of the outer class, a static package-access method is created in the outer class. This is access$000.

    Those two articles show both how the actual escaping occurs and what might happen.

    The 'what' is basically a race condition that could lead to a NullPointerException or any other exception when attempting to use the object while not yet fully initialized. In the example, if a thread is quick enough, it could happen that it runs the doSomething() method while num has not yet been correctly initialized to 42. In the first link there's a test that shows exactly that.

    EDIT: A few lines regarding how to code against this issue/feature were missing. I can only think about sticking to a (maybe incomplete) set of rules/principles to avoid this problem and others alike:

    • Only call private methods from within the constructor
    • If you like adrenaline and want to call protected methods from within the constructor, do it, but declare these methods as final, so that they cannot be overriden by subclasses
    • Never create inner classes in the constructor, either anonymous, local, static or non-static
    • In the constructor, don't pass this directly as an argument to anything
    • Avoid any transitive combination of the rules above, i.e. don't create an anonymous inner class in a private or protected final method that is invoked from within the constructor
    • Use the constructor to just construct an instance of the class, and let it only initialize attributes of the class, either with default values or with provided arguments

    If you need to do further things, use either the builder or the factory pattern.

    0 讨论(0)
  • 2020-11-28 07:17

    I'll modify the example a bit, to make it more clear. Consider this class:

    public class ThisEscape {
    
        Object someThing;
    
        public ThisEscape(EventSource source) {
            source.registerListener(
                new EventListener() {
                    public void onEvent(Event e) {
                        doSomething(e, someThing);
                    }
                });
            someThing = initTheThing();
        }
    }
    

    Behind the scenes, the anonymous inner class has access to the outer instance. You can tell this, because you can access the instance variable someThing and, as Shashank mentioned you can access the outer instance via ThisEscape.this.

    The problem is that by giving the anonymous inner class instance to the outside (in this case the EventSource object) it will also carry the ThisEscape instance with it.

    What can happen bad with it? Consider this implementation of EventSource below:

    public class SomeEventSource implements EventSource {
    
        EventListener listener;
    
        public void registerListener(EventListener listener) {
            this.listener = listener;
        }
    
        public void processEvent(Event e) {
            listener.onEvent(e);
        }
    
    }
    

    In ThisEscape's constructor we register an EventListener which will be stored in the listener instance variable.

    Now consider two threads. One is calling the ThisEscape constructor, while the other calls processEvent with some event. Also, let's say the JVM decides to switch from the first thread to the second one, right after the source.registerListener line and right before someThing = initTheThing(). The second thread now runs, and it will call the onEvent method, which as you can see, does something with someThing. But what is someThing? It's null, because the other thread did not finish initializing the object, so this will (probably) cause a NullPointerException, which is not actually what you want.

    To sum up: be careful that you don't escape objects that have not been fully initialized (or in other words, their constructor did not finish yet). One subtle way you could inadvertently do this, is by escaping anonymous inner classes from the constructor, which will implicitly escape the outer instance, which is not fully initialized.

    0 讨论(0)
  • 2020-11-28 07:25

    The key point here is that it is often easy to forget that an in-lined anonymous object still has a reference to its parent object and that's how this code fragment is exposing a not-yet-completely-initialised instance of itself.

    Imagine EventSource.registerListener immediately calls EventLister.doSomething()! That doSomething will be called on an object whose parent this is incomplete.

    public class ThisEscape {
    
        public ThisEscape(EventSource source) {
            // Calling a method
            source.registerListener(
                    // With a new object
                    new EventListener() {
                        // That even does something
                        public void onEvent(Event e) {
                            doSomething(e);
                        }
                    });
            // While construction is still in progress.
        }
    }
    

    Doing it this way would plug the hole.

    public class TheresNoEscape {
    
        public TheresNoEscape(EventSource source) {
            // Calling a method
            source.registerListener(
                    // With a new object - that is static there is no escape.
                    new MyEventListener());
        }
    
        private static class MyEventListener {
    
            // That even does something
            public void onEvent(Event e) {
                doSomething(e);
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题