首页  >  文章  >  Java  >  Java Cleaners:管理外部资源的现代方法

Java Cleaners:管理外部资源的现代方法

PHPz
PHPz原创
2024-08-14 12:40:32557浏览

本文的代码可以在 GitHub 上找到。
如果您是那种喜欢在查看示例之前了解事物工作原理的程序员,
介绍完后可以直接跳到幕后清洁工

  • 简介
  • 简单清洁工的实际应用
  • 清洁工,正确的方法
  • 清洁工,有效的方法
  • 幕后清洁工

介绍

想象一个场景,您有一个对象保存对外部资源(文件、套接字等)的引用。并且您希望控制一旦持有对象不再活动/不可访问时如何释放这些资源,如何在 Java 中实现这一点?在 Java 9 之前,程序员可以通过重写 Object 类的 Finalize() 方法来使用终结器。终结器有很多缺点,包括速度慢、不可靠和危险。这是 JDK 的实现者和使用者都讨厌的功能之一。

自 Java 9 起,Finalizer 已被弃用,程序员有更好的选择在 Cleaners 中实现此目的,Cleaner 提供了更好的方法来管理和处理清理/终结操作。清理器的工作模式是让资源持有对象注册自己及其相应的清理操作。然后,一旦应用程序代码无法访问这些对象,清理器将调用清理操作。
本文并不是要告诉您为什么 Cleaners 比 Finalizers 更好,尽管我会简要列出它们的一些差异。

终结者与清洁者

终结器 清洁工 标题> 终结器由垃圾收集器的线程之一调用,作为程序员,您无法控制哪个线程将调用您的终结逻辑 与终结器不同,使用 Cleaners,程序员可以选择控制调用清理逻辑的线程。 当对象实际被 GC 收集时,调用终结逻辑 当对象变为
Finalizers Cleaners
Finalizers are invoked by one of Garbage Collector’s threads, you as a programmer don’t have control over what thread will invoke your finalizing logic Unlike with finalizers, with Cleaners, programmers can opt to have control over the thread that invokes the cleaning logic.
Finalizing logic is invoked when the object is actually being collected by GC Cleaning logic is invoked when the object becomes Phantom Reachable, that is our application has no means to access it anymore
Finalizing logic is part of the object holding the resources Cleaning logic and its state are encapsulated in a separate object.
No registration/deregistration mechanism Provides means for registering cleaning actions and explicit invocation/deregistration
幻像可达时,即我们的应用程序无法再访问它时,将调用清理逻辑 最终逻辑是持有资源的对象的一部分 清理逻辑及其状态封装在一个单独的对象中。 没有注册/注销机制 提供注册清理操作和显式调用/取消注册的方法 表>

简单清洁工的实际应用

足够多的闲聊让我们看看清洁工的实际行动。

资源持有者

import java.lang.ref.Cleaner;

public class ResourceHolder {
 private static final Cleaner CLEANER = Cleaner.create();
        public ResourceHolder() {
            CLEANER.register(this, () -> System.out.println("I'm doing some clean up"));
        }
        public static void main(String... args) {
            ResourceHolder resourceHolder = new ResourceHolder();
            resourceHolder = null;
            System.gc();
        }}

几行代码,但这里发生了很多事情,让我们分解它

  1. 常量 CLEANER 是 java.lang.ref.Cleaner 类型,从它的名字就可以看出, 这是 Java 中 Cleaners 功能的中心和起点。 CLEANER 变量被声明为静态变量,Cleaner 永远不应该是实例变量, 它们应该尽可能地在不同的班级之间共享。
  2. 在构造函数中,ResourceHolder 的实例将自身及其清理操作注册到 Cleaner,清理操作是一个 Runnable 作业,Cleaner 保证最多调用一次(最多一次,意味着可以不运行根本没有)。 通过调用 Cleaner 的 register() 方法,这些实例基本上向 Cleaner 说了两件事
    • 只要我还活着,就关注我
    • 一旦我不再活跃(Phantom Reachable),请尽力调用我的清洁操作。
  3. 在main方法中我们实例化了一个ResourceHolder对象,并立即将其变量设置为null,由于该对象只有一个变量引用,我们的应用程序无法再访问该对象,即它已变成幻影可达
  4. 我们调用 System.gc() 来请求 JVM 运行垃圾收集器, 因此,这将触发清洁器运行清洁操作。 通常,您不需要调用 System.gc(),而是像我们的应用程序一样简单, 我们需要帮助 Cleaner 执行该操作

运行应用程序,希望您看到我正在标准输出中的某处进行一些清理。

?注意
我们从最简单的方式开始使用 Cleaners,因此我们可以以简化的方式演示其用法,但请记住,尽管这既不是有效的也不是正确的使用 Cleaners

清洁工,正确的方法

我们的第一个示例足以看到清洁工的实际效果,
但正如我们警告的那样,这不是在实际应用程序中使用 Cleaners 的正确方法。
让我们看看我们所做的有什么问题。

  1. 我们启动了一个 Cleaner 对象作为 ResourceHolder 的类成员:正如我们之前提到的,Cleaner 应该在类之间共享,而不应该属于单个类,原因是每个 Cleaner 实例都维护一个线程,这是有限的本机资源,并且在消耗原生资源时要小心。
    在真实的应用程序中,我们通常从实用程序或 Singleton 类中获取 Cleaner 对象,例如

      private static CLEANER = AppUtil.getCleaner();
    
  2. 我们传入 lambda 作为我们的清洁操作:您不应该永远传入 lambda 作为您的清洁操作。
    要了解原因,
    让我们重构之前的示例,提取打印出的消息并将其设为实例变量

    资源持有者

    public class ResourceHolder {
       private static final Cleaner CLEANER = Cleaner.create();
       private final String cleaningMessage = "I'm doing some clean up";
       public ResourceHolder() {
           CLEANER.register(this, () -> System.out.println(cleaningMessage));
       }
    }
    

    运行应用程序,看看会发生什么。
    我会告诉你发生了什么,
    无论您运行应用程序多少次,清洁操作都永远不会被调用。
    让我们看看为什么

    • 在内部,Cleaner 利用 PhantomReference 和 ReferenceQueue 来跟踪已注册的对象, 一旦对象变为幻影可到达,ReferenceQueue将通知Cleaner Cleaner 将使用其线程来运行相应的清洁操作。
    • 通过让 lambda 访问实例成员,我们强制 lambda 保存(ResourceHolder 实例的) this 引用,因此该对象永远不会成为 Phantom Reachable 因为我们的应用程序代码仍然引用它。

    ?注意
    如果您仍然想知道在我们的第一个示例中,尽管将清理操作作为 lambda 进行调用,但它是如何被调用的。原因是,第一个示例中的 lambda 不访问任何实例变量,并且与内部类不同,Lambda 不会隐式保存包含的对象引用,除非被迫这样做。

    正确的方法是将清洁操作及其所需的状态封装在静态嵌套类中。

    ?警告
    不要使用匿名内部类,它比使用 lambda 更糟糕,因为内部类实例将保存对外部类实例的引用,无论它们是否访问其实例变量。

  3. We didn't make use of the return value from the Cleaner.create(): The create() actually returns something very important.a Cleanable object, this object has a clean() method that wraps your cleaning logic, you as a programmer can opt to do the cleanup yourself by invoking the clean() method. As mentioned earlier, another thing that makes Cleaners superior to Finalizers is that you can actually deregister your cleaning action. The clean() method actually deregisters your object first, and then it invokes your cleaning action, this way it guarantees the at-most once behavior.

Now let us improve each one of these points and revise our ResourceHolder class

ResourceHolder

import java.lang.ref.Cleaner;

public class ResourceHolder {

    private final Cleaner.Cleanable cleanable;
    private final ExternalResource externalResource;

    public ResourceHolder(ExternalResource externalResource) {
        cleanable = AppUtil.getCleaner().register(this, new CleaningAction(externalResource));
        this.externalResource = externalResource;
    }

//    You can call this method whenever is the right time to release resource
    public void releaseResource() {
        cleanable.clean();
    }

    public void doSomethingWithResource() {
        System.out.printf("Do something cool with the important resource: %s \n", this.externalResource);
    }

    static class CleaningAction implements Runnable {
        private ExternalResource externalResource;

        CleaningAction(ExternalResource externalResource) {
            this.externalResource = externalResource;
        }

        @Override
        public void run() {
//          Cleaning up the important resources
            System.out.println("Doing some cleaning logic here, releasing up very important resource");
            externalResource = null;
        }
    }

    public static void main(String... args) {
        ResourceHolder resourceHolder = new ResourceHolder(new ExternalResource());
        resourceHolder.doSomethingWithResource();
/*
        After doing some important work, we can explicitly release
        resources/invoke the cleaning action
*/
        resourceHolder.releaseResource();
//      What if we explicitly invoke the cleaning action twice?
        resourceHolder.releaseResource();
    }
}

ExternalResource is our hypothetical resource that we want to release when we’re done with it.
The cleaning action is now encapsulated in its own class, and we make use of the CleaniangAction object, we call it’s clean() method in the releaseResources() method to do the cleanup ourselves.
As stated earlier, Cleaners guarantee at most one invocation of the cleaning action, and since we call the clean() method explicitly the Cleaner will not invoke our cleaning action except in the case of a failure like an exception is thrown before the clean method is called, in this case the Cleaner will invoke our cleaning action when the ResourceHolder object becomes Phantom Reachable, that is we use the Cleaner as our safety-net, our backup plan when the first plan to clean our own mess doesn’t work.

❗ IMPORTANT
The behavior of Cleaners during System.exit is implementation-specific. With this in mind, programmers should always prefer to explicitly invoke the cleaning action over relying on the Cleaners themselves..

Cleaners, the effective way

By now we’ve established the right way to use Cleaners is to explicitly call the cleaning action and rely on them as our backup plan.What if there’s a better way? Where we don’t explicitly call the cleaning action, and the Cleaner stays intact as our safety-net.
This can be achieved by having the ResourceHolder class implement the AutoCloseable interface and place the cleaning action call in the close() method, our ResourceHolder can now be used in a try-with-resources block. The revised ResourceHolder should look like below.

ResourceHolder

import java.lang.ref.Cleaner.Cleanable;

public class ResourceHolder implements AutoCloseable {

    private final ExternalResource externalResource;

    private final Cleaner.Cleanable cleanable;

    public ResourceHolder(ExternalResource externalResource) {
        this.externalResource = externalResource;
        cleanable = AppUtil.getCleaner().register(this, new CleaningAction(externalResource));
    }

    public void doSomethingWithResource() {
        System.out.printf("Do something cool with the important resource: %s \n", this.externalResource);
    }
    @Override
    public void close() {
        System.out.println("cleaning action invoked by the close method");
        cleanable.clean();
    }

    static class CleaningAction implements Runnable {
        private ExternalResource externalResource;

        CleaningAction(ExternalResource externalResource) {
            this.externalResource = externalResource;
        }

        @Override
        public void run() {
//            cleaning up the important resources
            System.out.println("Doing some cleaning logic here, releasing up very important resources");
            externalResource = null;
        }
    }

    public static void main(String[] args) {
//      This is an effective way to use cleaners with instances that hold crucial resources
        try (ResourceHolder resourceHolder = new ResourceHolder(new ExternalResource(1))) {
            resourceHolder.doSomethingWithResource();
            System.out.println("Goodbye");
        }
/*
    In case the client code does not use the try-with-resource as expected,
    the Cleaner will act as the safety-net
*/
        ResourceHolder resourceHolder = new ResourceHolder(new ExternalResource(2));
        resourceHolder.doSomethingWithResource();
        resourceHolder = null;
        System.gc(); // to facilitate the running of the cleaning action
    }
}


Cleaners behind the scene

? NOTE
To understand more and see how Cleaners work, checkout the OurCleaner class under the our_cleaner package that imitates the JDK real implementation of Cleaner. You can replace the real Cleaner and Cleanable with OurCleaner and OurCleanable respectively in all of our examples and play with it.

Let us first get a clearer picture of a few, already mentioned terms, phantom-reachable, PhantomReference and ReferenceQueue

  • Consider the following code

    Object myObject = new Object();
    

    In the Garbage Collector (GC) world the created instance of Object is said to be strongly-reachable, why? Because it is alive, and in-use i.e., Our application code has a reference to it that is stored in the myObject variable, assume we don’t set another variable and somewhere in our code this happens

    myObject = null;
    

    The instance is now said to be unreachable, and is eligible for reclamation by the GC.
    Now let us tweak the code a bit

    Object myObject = new Object();
    PhantomReference<Object> reference = new PhantomReference<>(myObject, null);
    

    Reference is a class provided by JDK to represent reachability of an object during JVM runtime, the object a Reference object is referring to is known as referent, PhantomReference is a type(also an extension) of Reference whose purpose will be explained below in conjunction with ReferenceQueue.
    Ignore the second parameter of the constructor for now, and again assume somewhere in our code this happens again

    myObject = null;
    

    Now our object is not just unreachable it is phantom-reachable because no part of our application code can access it, AND it is a referent of a PhantomReference object.

  • After the GC has finalized a phantom-reachable object, the GC attaches its PhantomReference object(not the referent) to a special kind of queue called ReferenceQueue. Let us see how these two concepts work together

    Object myObject = new Object();
    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> reference1 = new PhantomReference<>(myObject, queue);
    myObject = null;
    PhantomReference<Object> reference2 = (PhantomReference)queue.remove()
    

    We supply a ReferenceQueue when we create a PhantomReference object so the GC knows where to attach it when its referent has been finalized. The ReferenceQueue class provides two methods to poll the queue, remove(), this will block when the queue is empty until the queue has an element to return, and poll() this is non-blocking, when the queue is empty it will return null immediately.
    With that explanation, the code above should be easy to understand, once myObject becomes phantom-reachable the GC will attach the PhantomReference object to queue and we get it by using the remove() method, that is to say reference1 and reference2 variables refer to the same object.

Now that these concepts are out of the way, let’s explain two Cleaner-specific types

  1. For each cleaning action, Cleaner will wrap it in a Cleanable instance, Cleanable has one method, clean(), this method ensure the at-most once invocation behavior before invoking the cleaning action.
  2. PhantomCleanable implements Cleanable and extends PhantomReference, this class is the Cleaner’s way to associate the referent(resource holder) with their cleaning action

From this point on understanding the internals of Cleaner should be straight forward.

Cleaner Life-Cycle Overview

Java Cleaners: The Modern Way to Manage External Resources

Let us look at the life-cycle of a Cleaner object

  • The static Cleaner.create() method instantiates a new Cleaner but it also does a few other things

    • It instantiates a new ReferenceQueue, that the Cleaner objet’s thread will be polling
    • It creates a doubly linked list of PhantomCleanable objects, these objects are associated with the queue created from the previous step.
    • It creates a PhantomCleanable object with itself as the referent and empty cleaning action.
    • It starts a daemon thread that will be polling the ReferenceQueue as long as the doubly linked list is not empty.

    By adding itself into the list, the cleaner ensures that its thread runs at least until the cleaner itself becomes unreachable

  • For each Cleaner.register() call, the cleaner creates an instance of PhantomCleanable with the resource holder as the referent and the cleaning action will be wrapped in the clean() method, the object is then added to the aforementioned linked list.

  • The Cleaner’s thread will be polling the queue, and when a PhantomCleanable is returned by the queue, it will invoke its clean() method. Remember the clean() method only calls the cleaning action if it manages to remove the PhantomCleanable object from the linked list, if the PhantomCleanable object is not on the linked list it does nothing

  • The thread will continue to run as long as the linked list is not empty, this will only happen when

    • All the cleaning actions have been invoked, and
    • The Cleaner itself has become phantom-reachable and has been reclaimed by the GC

以上是Java Cleaners:管理外部资源的现代方法的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn