问题来源

之前的一篇文章讲了 利用 WeakReference 关闭守护线程 ,守护线程一旦发现守护的对象不在了,就把自己清理掉。

这次的问题更棘手一些,假如一个对象有一些资源需要被关闭,那怎么处理?

很多人会说,这个简单啊!用 Java 的finalize

但在 Java 中的finalize真的设计得不好,一不小心就会引发很多问题。

 

Java 中的finalize有哪些问题?

  1. 影响 GC 性能,可能会引发OutOfMemoryException
  2. finalize方法中对异常处理不当会影响 GC
  3. 子类中未调用super.finalize会导致父类的finalize得不到执行

总结一下就是:实现finalize对代码的质量要求非常高,一旦使用不当,就容易引发各种问题。

 

PhantomReference

Java 中的各种引用的区别就不说了,网上一搜一大堆。 直接上代码吧。

假设我有这样一个类,内部有一个InputStream并且需要自动close掉它。你只需要这么用就行了:

public class CleanUpExample {
    private InputStream input;

    public CleanUpExample() {
        //todo:init input
        CleanUpHelper.register(this, new CleanUpImpl(input));
    }

    static class CleanUpImpl implements CleanUp {
        private final InputStream input;

        public CleanUpImpl(InputStream input) {
            this.input = input;
        }

        @Override
        public void cleanUp() {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            System.out.println("Success!");
        }
    }
}

看完了业务代码就来看看底层实现吧,先看一下最简单的CleanUp接口:

public interface CleanUp {
    void cleanUp();
}

然后看一下略复杂的CleanUpHelper

public final class CleanUpHelper {

    private CleanUpHelper(){}

    private static volatile boolean started = false;

    private static final int SLEEP_TIME = 10;

    private static final Thread CLEAN_UP_THREAD = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Reference target = REFERENCE_QUEUE.poll();
                    if (target != null) {
                        CleanUp cleanUp = MAPS.remove(target);
                        if (cleanUp != null) {
                            cleanUp.cleanUp();
                            continue;
                        }
                    }
                } catch (RuntimeException ignore) {
                    //add logs
                }

                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    });

    private static final Map<Reference<Object>, CleanUp> MAPS = new ConcurrentHashMap<Reference<Object>, CleanUp>();

    private static final ReferenceQueue<Object> REFERENCE_QUEUE = new ReferenceQueue<Object>();

    public static void register(Object watcher, CleanUp cleanUp) {
        init();
        MAPS.put(new PhantomReference<Object>(watcher, REFERENCE_QUEUE), cleanUp);
    }

    private static void init() {
        if (!started) {
            synchronized (CleanUpHelper.class) {
                if (!started) {
                    CLEAN_UP_THREAD.setName("CleanUpThread");
                    CLEAN_UP_THREAD.setDaemon(true);
                    CLEAN_UP_THREAD.start();
                    started = true;
                }
            }
        }
    }
}

最后跑一下测试代码,看看是否能被清理掉:

CleanUpExample item = new CleanUpExample();
item = null;
System.gc();
Thread.sleep(2000);

 

使用过程中的一个坑

CleanUpExample 在使用过程中只要实现一下CleanUp接口并且注册一下即可。

看似简单但这里有一个大坑,创建内部类的时候,一定要用静态内部类,而不要使用匿名内部类、成员内部类和局部内部类。

因为只有静态内部类才不会依赖外围类,其它的内部类在编译完成后会隐含地保存着一个引用,该引用是指向创建它的外围内。

这样你的代码又把CleanUpImpl注册到了CleanUpHelper中,最终导致CleanUpExample无法被 GC。

来一个错误的例子:

public class CleanUpExample {
    private InputStream input;

    public CleanUpExample() {
        //todo:init input
        
        CleanUpHelper.register(this, new CleanUp() {
            @Override
            public void cleanUp() {
                try {
                    if (input != null) {
                        input.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

                System.out.println("Success!");
            }
        });
    }
}

我第一次用自己写的CleanUpHelper就是这么写的,匿名内部类多简洁啊,但是,这样写后就无法生效了,一定要注意!

 

为什么不用close方法来解决。

之前提到的守护进程和这次的资源清理,其实只要加一个close方法,在销毁的时候调一下就行了。

但是我们现在做的都是给全公司用的 Java 中间件。用户是不爱看文档的,我以前用别人的中间件也不看;用户也很少回去完整地在finally中去调用close方法。我自己也不喜欢,懒癌发作。

本作品由 Dozer 创作,采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。