”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Java Cleaners:管理外部资源的现代方法

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

发布于2024-08-21
浏览:996

Code for this article can be found on GitHub.
If you’re the type of programmer who likes to understand the internals of how things work before seeing examples,
you can jump directly to Cleaners behind the scene after the introduction.

  • Introduction
  • Simple Cleaner in action
  • Cleaners, the right way
  • Cleaners, the effective way
  • Cleaners behind the scene

Introduction

Think of a scenario where you have an object that holds references to external resources (files, sockets, and so on). And you want to have control over how these resources are released once the holding object is no longer active/accessible, how do you achieve that in Java?. Prior to Java 9 programmers could use a finalizer by overriding the Object’s class finalize() method. Finalizers have many disadvantages, including being slow, unreliable and dangerous. It is one of those features that are hated by both those who implement the JDK and those who use it.

Since Java 9, Finalizers have been deprecated and programmers have a better option to achieve this in Cleaners, Cleaners provide a better way to manage and handle cleaning/finalizing actions. Cleaners work in a pattern where they let resource holding objects register themselves and their corresponding cleaning actions. And then Cleaners will call the cleaning actions once these objects are not accessible by the application code.
This is not the article to tell you why Cleaners are better than Finalizers, though I will briefly list some of their differences.

Finalizers Vs Cleaners

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

Simple Cleaner in action

Enough chit-chats let us see Cleaners in action.

ResourceHolder

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();
        }}

Few lines of code but a lot is happening here, Let us break it down

  1. The constant CLEANER is of type java.lang.ref.Cleaner, as you can tell from its name, this is the central and starting point of the Cleaners feature in Java. The CLEANER variable is declared as static as it should be, Cleaners should never be instance variables, they should be shared across different classes as much as possible.
  2. In the constructor, instances of ResourceHolder are registering themselves to the Cleaner along with their cleaning action, the cleaning action is a Runnable job that the Cleaner guarantees to invoke at most once (at most once, meaning it is possible not to run at all). By calling Cleaner’s register() method, these instances are basically saying two things to the Cleaner
    • Keep track of me as long as I live
    • And once I am no longer active (Phantom Reachable), please do your best and invoke my cleaning action.
  3. In the main method we instantiate an object of ResourceHolder and immediately set its variable to null, since the object has only one variable reference, our application can no longer access the object, i.e., it has become Phantom Reachable
  4. We call System.gc() to request JVM to run the Garbage Collector, consequentially this will trigger the Cleaner to run the cleaning action. Typically, you don’t need to call System.gc() but as simple as our application, we need to facilitate the Cleaner to run the action

Run the application, and hopefully you see I’m doing some clean up somewhere in your standard output.

? CAUTION
We started with the simplest possible way to use Cleaners, so we can demonstrate its usage in a simplified way, bear in mind though this is neither effective nor the right way to use Cleaners

Cleaners, the right way

Our first example was more than good enough to see Cleaners in action,
but as we warned, it is not the right way to use Cleaners in a real application.
Let’s see what is wrong with what we did.

  1. We initiated a Cleaner object as a class member of the ResourceHolder: As we mentioned earlier Cleaners should be shared across Classes and should not belong to individual classes, reason behind being each Cleaner instance maintains a thread, which is a limited native resource, and you want to be cautious when you consume native resources.
    In a real application, we typically get a Cleaner object from a utility or a Singleton class like

      private static CLEANER = AppUtil.getCleaner();
    
  2. We passed in a lambda as our Cleaning action: You should NEVER pass in a lambda as your cleaning action.
    To understand why,
    let us refactor our previous example by extracting the printed out message and make it an instance variable

    ResourceHolder

    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));
       }
    }
    

    Run the application and see what happens.
    I will tell you what happens,
    the cleaning action will never get invoked no matter how many times you run your application.
    Let us see why

    • Internally, Cleaners make use of PhantomReference and ReferenceQueue to keep track of registered objects, once an object becomes Phantom Reachable the ReferenceQueue will notify the Cleaner and the Cleaner will use its thread to run the corresponding cleaning action.
    • By having the lambda accessing the instance member we’re forcing the lambda to hold the this reference(of ResourceHolder instance), because of this the object will never ever become Phantom Reachable because our Application code still has reference to it.

    ? NOTE
    If you still wonder how in our first example, the cleaning action is invoked despite having it as a lambda. The reason is, the lambda in the first example does not access any instance variable, and unlike inner classes, Lambdas won’t implicitly hold the containing object reference unless they’re forced to.

    The right way is to encapsulate your cleaning action together with the state it needs in a static nested class.

    ? Warning
    Don’t use inner class anonymous or not, it is worse than using lambda because an inner class instance would hold a reference to the outer class instance regardless of whether they access their instance variable or not.

  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 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 queue = new ReferenceQueue();
    PhantomReference reference1 = new PhantomReference(myObject, queue);
    myObject = null;
    PhantomReference 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
版本声明 本文转载于:https://dev.to/ahmedjaad/java-cleaners-the-modern-way-to-manage-external-resources-4d4?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 在 Go 中使用 WebSocket 进行实时通信
    在 Go 中使用 WebSocket 进行实时通信
    构建需要实时更新的应用程序(例如聊天应用程序、实时通知或协作工具)需要一种比传统 HTTP 更快、更具交互性的通信方法。这就是 WebSockets 发挥作用的地方!今天,我们将探讨如何在 Go 中使用 WebSocket,以便您可以向应用程序添加实时功能。 在这篇文章中,我们将介绍: WebSoc...
    编程 发布于2024-11-16
  • Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta:列偏移的删除和恢复Bootstrap 4 在其 Beta 1 版本中引入了重大更改柱子偏移了。然而,随着 Beta 2 的后续发布,这些变化已经逆转。从 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    编程 发布于2024-11-16
  • 除了“if”语句之外:还有什么地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    除了“if”语句之外:还有什么地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    无需强制转换即可上下文转换为 bool您的类定义了对 bool 的显式转换,使您能够在条件语句中直接使用其实例“t”。然而,这种显式转换提出了一个问题:“t”在哪里可以在不进行强制转换的情况下用作 bool?上下文转换场景C 标准指定了四种值可以根据上下文转换为的主要场景bool:语句:if、whi...
    编程 发布于2024-11-16
  • 如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    MySQL配置不正确:相对路径的问题在Django中运行python manage.py runserver时,可能会遇到以下错误:ImproperlyConfigured: Error loading MySQLdb module: dlopen(/Library/Python/2.7/site-...
    编程 发布于2024-11-16
  • “易失性”在 C++11 多线程中仍然相关吗?
    “易失性”在 C++11 多线程中仍然相关吗?
    C 11 中的 Volatile 变量C 11 标准中多线程机器模型的引入引发了有关 volatile 行为的问题变量,传统上用于防止可能导致并发环境中未定义行为的优化。在 C 98/03 中,内存模型中缺乏对多线程的识别意味着编译器可以优化无法读取易失性变量,从而导致臭名昭著的无休止 while ...
    编程 发布于2024-11-16
  • 如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    在 PHP 中组合关联数组在 PHP 中,将两个关联数组组合成一个数组是一项常见任务。考虑以下请求:问题描述:提供的代码定义了两个关联数组,$array1和$array2。目标是创建一个新数组 $array3,它合并两个数组中的所有键值对。 此外,提供的数组具有唯一的 ID,而名称可能重合。要求是构...
    编程 发布于2024-11-16
  • 如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 识别今天生日的用户使用 MySQL 确定今天是否是用户的生日涉及查找生日匹配的所有行今天的日期。这可以通过一个简单的 MySQL 查询来实现,该查询将存储为 UNIX 时间戳的生日与今天的日期进行比较。以下 SQL 查询将获取今天有生日的所有用户: FROM USERS ...
    编程 发布于2024-11-16
  • 大批
    大批
    方法是可以在对象上调用的 fns 数组是对象,因此它们在 JS 中也有方法。 slice(begin):将数组的一部分提取到新数组中,而不改变原始数组。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index p...
    编程 发布于2024-11-16
  • 为什么 Visual Studio 2010 中 x86 和 x64 的浮点运算不同?
    为什么 Visual Studio 2010 中 x86 和 x64 的浮点运算不同?
    x86 和 x64 之间的浮点算术差异在 Visual Studio 2010 中,x86 和 x64 版本之间的浮点算术存在明显差异当比较某些表达式的值时出现。这种差异体现在以下代码中:float a = 50.0f; float b = 65.0f; float c = 1.3f; float ...
    编程 发布于2024-11-15
  • 如何提高带有通配符的 MySQL LIKE 运算符的性能?
    如何提高带有通配符的 MySQL LIKE 运算符的性能?
    MySQL LIKE 运算符优化问题:使用通配符(例如 '%test% ')?答案: 是的,在查询中使用特定模式时,MySQL 可以优化 LIKE 运算符的性能。前缀通配符: 如果您的查询类似于 foo LIKE 'abc%' 或 foo LIKE 'abc�...
    编程 发布于2024-11-15
  • 如何使用 PHP 通过 POST 向外部网站发送数据?
    如何使用 PHP 通过 POST 向外部网站发送数据?
    在 PHP 中通过 POST 重定向和发送数据在 PHP 中,您可能会遇到需要将用户重定向到外部的情况网站并通过 POST 将数据传递到该网站。与 HTML 表单不同,PHP 本身并不支持此行为。GET 与 POST在 Web 开发中,有两种主要方法用于从源发送数据到目的地:GET:数据作为查询参数...
    编程 发布于2024-11-15
  • 如何使用 GCC 捕获 Linux 中的分段错误?
    如何使用 GCC 捕获 Linux 中的分段错误?
    捕获 Linux 中的分段错误问:我在第三方库中遇到分段错误,但我无法解决根本问题。是否有跨平台或特定于平台的解决方案来使用 gcc 捕获 Linux 中的这些错误?A:Linux 允许将分段错误作为异常处理。当程序遇到此类故障时,它会收到 SIGSEGV 信号。通过设置信号处理程序,您可以拦截此信...
    编程 发布于2024-11-15
  • 如何在不创建实例的情况下访问Go结构体的类型?
    如何在不创建实例的情况下访问Go结构体的类型?
    在不创建物理结构的情况下访问 Reflect.Type在 Go 中,动态加载问题的解决方案需要访问结构的类型,而无需物理创建它们。虽然现有的解决方案要求在类型注册之前创建结构体并清零,但存在一种更有效的方法。人们可以利用 reflect.TypeOf((*Struct)(nil)).Elem()手术...
    编程 发布于2024-11-15
  • Java中如何高效地将整数转换为字节数组?
    Java中如何高效地将整数转换为字节数组?
    Java 中整数到字节数组的高效转换将整数转换为字节数组可用于多种目的,例如网络传输或数据存储。有多种方法可以实现此转换。ByteBuffer 类:一种有效的方法是使用 ByteBuffer 类。 ByteBuffer 是一个存储二进制数据并提供各种操作来操纵它的缓冲区。使用 ByteBuffer ...
    编程 发布于2024-11-15
  • 如何在 Go 中按多个字段对结构体切片进行排序?
    如何在 Go 中按多个字段对结构体切片进行排序?
    按多个字段对切片对象进行排序按多个条件排序考虑以下 Parent 和 Child 结构:type Parent struct { id string children []Child } type Child struct { id string }假设我们有一个...
    编程 发布于2024-11-15

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3