Java 线程安全性中的对象发布和逸出

“发布(Publish)” 一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法返回该引用,或者将引用传递到其他类的方法中。在许多情况中,我们要确保对象及其内部状态不被发布。而在某些情况下,我们又需要发布某个对象,但如果在发布时要确保线程安全性,则可能需要同步。当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。

我们首先来看看一个对象是如何逸出的。

发布对象的最简单的办法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象,如下面程序所示:

    public static Set knownSecrets;

    public void initialize() {
        knownSecrets = new HashSet();
    }

当发布某个对象时,可能会间接地发布其他对象。如果将一个 Secret 对象添加集合 knownSecrets 中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新 Secret 对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象。如下的 UnsafeStates 发布了本应为私有的状态数组。

    class UnsafeStates {
        private Sting[] states = new String[] {
            "AK", "AL"
        };

        public String[] getStates() {
            return states;
        }
    }

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。

无论其他的线程是否会对已发布的引用执行何种操作,其实都不重要,因为误用该引用的风险始终存在。当某个对象逸出后,你必须假设有某个类或线程可能会无用该对象。

最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。如下面实例。

    public class ThisEscape {

        private int value = 0;

        public ThisEscape(EventSource source) {
            source.registerListener(new EventListener() {
                @Override
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
            this.value = 7;
        }

        void doSomething(Event e) {
            System.out.println(this.value);
        }

        interface EventSource {
            void registerListener(EventListener e);
        }

        interface EventListener {
            void onEvent(Event e);
        }

        interface Event {
        }

        public static void main(String[] args) {
            Event event = new Event() {};
            EventSource source = new EventSource() {
                @Override
                public void registerListener(EventListener e) {
                    e.onEvent(event);
                }
            };
            ThisEscape escape = new ThisEscape(source); 
            System.out.println(escape.value);
        }

    }   

在 ThisEscape 中给出了逸出的一个特殊示例,即 this 引用在构造函数中逸出。当内部的 EventListener 实例发布时,在外部封装的 ThisEscape 实例也逸出了。当且仅当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确构造。

对于上面的ThisEscape,们可以使用工厂方法来防止 this 引用在构造过程中逸出。

    public class SafeListener {

        private int value = 0;

        private final EventListener listener;

        private SafeListener() {
            listener = new EventListener() {
                @Override
                public void onEvent(Event e) {
                    doSomething(e);
                }
            };

            this.value = 7;
        }

        public static SafeListener newInstance(EventSource source) {
            SafeListener safe = new SafeListener();
            source.registerListener(safe.listener);
            return safe;
        }

        void doSomething(Event e) {
            System.out.println(this.value);
        }

        interface EventSource {
            void registerListener(EventListener e);
        }

        interface EventListener {
            void onEvent(Event e);
        }

        interface Event {
        }

        public static void main(String[] args) {
            Event event = new Event() {};
            EventSource source = new EventSource() {
                @Override
                public void registerListener(EventListener e) {
                    e.onEvent(event);
                }
            };
            SafeListener safe = SafeListener.newInstance(source);
            System.out.println(safe.value);
        }

    }   

在构造过程中使 this 引用逸出的另一个常见错误是,在构造函数中启动一个线程。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个 start 或者 initialize 方法来启动。

猜你喜欢

转载自blog.csdn.net/xxc1605629895/article/details/80904319