首頁 > Java > java教程 > 主體

Java Cleaners:管理外部資源的現代方法

PHPz
發布: 2024-08-14 12:40:32
原創
595 人瀏覽過

本文的程式碼可以在 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中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板