Java Cleaners: 外部リソースを管理する最新の方法

PHPz
リリース: 2024-08-14 12:40:32
オリジナル
595 人が閲覧しました

この記事のコードは GitHub にあります。
あなたが、例を見る前に物事がどのように機能するのか内部を理解したいタイプのプログラマーであれば、
導入後は、舞台裏のクリーナーに直接ジャンプできます。

  • はじめに
  • シンプルクリーナーの動作中
  • クリーナー、正しい方法
  • クリーナー、効果的な方法
  • 舞台裏の清掃員

導入

外部リソース (ファイル、ソケットなど) への参照を保持するオブジェクトがあるシナリオを考えてみましょう。また、保持しているオブジェクトがアクティブまたはアクセスできなくなったときに、これらのリソースがどのように解放されるかを制御したいと考えています。Java ではどうすればよいでしょうか? Java 9 より前では、プログラマはオブジェクトのクラスの Finalize() メソッドをオーバーライドすることでファイナライザーを使用できました。ファイナライザーには、遅い、信頼性が低い、危険であるなど、多くの欠点があります。これは、JDK を実装する人と使用する人の両方から嫌われている機能の 1 つです。

Java 9 以降、ファイナライザーは非推奨になり、プログラマーはクリーナーでこれを実現するためのより良いオプションを利用できます。クリーナーは、クリーニング/ファイナライズ アクションを管理および処理するためのより良い方法を提供します。クリーナーは、オブジェクトを保持するリソースに自身とそれに対応するクリーニング アクションを登録させるパターンで動作します。そして、これらのオブジェクトがアプリケーション コードからアクセスできなくなると、クリーナーはクリーニング アクションを呼び出します。
これは、クリーナーがファイナライザーよりも優れている理由を説明する記事ではありませんが、違いのいくつかを簡単にリストします。

ファイナライザー vs クリーナー

ファイナライザー クリーナー ファイナライザーはガベージ コレクターのスレッドの 1 つによって呼び出されます。プログラマーは、どのスレッドがファイナライズ ロジックを呼び出すかを制御できません。 ファイナライザーとは異なり、クリーナーを使用すると、プログラマはクリーニング ロジックを呼び出すスレッドを制御することを選択できます。 ファイナライズ ロジックは、オブジェクトが実際に 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 のクリーナー機能の中心であり開始点です。 CLEANER 変数は静的として宣言されています。クリーナーはインスタンス変数であってはなりません。 可能な限り、異なるクラス間で共有する必要があります。
  2. コンストラクターでは、ResourceHolder のインスタンスがクリーニング アクションとともにクリーナーに登録されます。クリーニング アクションは、クリーナーが最大 1 回 (最大 1 回、つまり実行しなくても可能であることを意味します) 呼び出すことが保証されている実行可能なジョブです。まったく)。 Cleaner の register() メソッドを呼び出すことにより、これらのインスタンスは基本的に Cleaner に対して 2 つのことを伝えます。
    • 私が生きている限り私を追跡してください
    • そして、私が活動できなくなったら (幻覚到達可能)、最善を尽くして私の浄化アクションを発動してください。
  3. メインメソッドでは、ResourceHolder のオブジェクトをインスタンス化し、すぐにその変数を null に設定します。オブジェクトには変数参照が 1 つしかないため、アプリケーションはオブジェクトにアクセスできなくなります。つまり、オブジェクトは になります。ファントム到達可能
  4. System.gc() を呼び出して、JVM にガベージ コレクターの実行を要求します。 その結果、これによりクリーナーがクリーニング アクションを実行するようになります。 通常、System.gc() を呼び出す必要はありませんが、このアプリケーションと同じくらい簡単です。 クリーナーがアクションを実行できるようにする必要があります

アプリケーションを実行すると、標準出力のどこかでクリーンアップを行っていることがわかると思います。

?注意
クリーナーの最も簡単な使用方法から始めたので、簡単な方法でその使用法を説明します。ただし、これはクリーナーの効果的または正しい使用方法ではないことに留意してください

クリーナー、正しい方法

最初の例は、クリーナーの動作を確認するには十分以上のものでした。
しかし、警告したように、実際のアプリケーションでクリーナーを使用するのは正しい方法ではありません。
私たちがやったことのどこが間違っているのか見てみましょう。

  1. Cleaner オブジェクトを ResourceHolder のクラス メンバーとして開始しました。前述したように、Cleaner はクラス間で共有されるべきであり、個々のクラスに属すべきではありません。その背後にある理由は、各 Cleaner インスタンスが限られたネイティブ リソースであるスレッドを維持するためです。ネイティブ リソースを消費するときは注意が必要です。
    実際のアプリケーションでは、通常、ユーティリティまたは
    のようなシングルトン クラスから Cleaner オブジェクトを取得します。

      private static CLEANER = AppUtil.getCleaner();
    
    ログイン後にコピー
  2. クリーニング アクションとしてラムダを渡しました。クリーニング アクションとしてラムダを決して渡さないでください。
    その理由を理解するには、
    出力されたメッセージを抽出して、それをインスタンス変数にして、前の例をリファクタリングしましょう

    リソースホルダー

    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));
       }
    }
    
    ログイン後にコピー

    アプリケーションを実行して、何が起こるかを確認してください。
    何が起こるかお話します
    アプリケーションを何度実行しても、クリーニング アクションは決して呼び出されません。
    その理由を見てみましょう

    • 内部では、クリーナーは PhantomReference と ReferenceQueue を利用して、登録されたオブジェクトを追跡します。 オブジェクトが Phantom Reachable になると、ReferenceQueue が Cleaner に通知します。 そして、クリーナーはそのスレッドを使用して、対応するクリーニング アクションを実行します。
    • ラムダがインスタンス メンバーにアクセスできるようにすることで、ラムダに (ResourceHolder インスタンスの) this 参照を強制的に保持させます。このため、オブジェクトが Phantom Reachable<🎜 になることはありません。 > アプリケーションコードがまだそれを参照しているためです。

    ?注
    最初の例で、ラムダであるにもかかわらず、クリーニング アクションがどのように呼び出されるのかまだ疑問に思われる方のために。その理由は、最初の例のラムダはインスタンス変数にアクセスせず、内部クラスとは異なり、強制されない限り、ラムダはそれを含むオブジェクト参照を暗黙的に保持しないためです。
    正しい方法は、クリーニング アクションを必要な状態と一緒に静的なネストされたクラスにカプセル化することです。


    ?警告
    匿名の内部クラスを使用しないでください。内部クラスのインスタンスは、インスタンス変数にアクセスするかどうかに関係なく、外部クラスのインスタンスへの参照を保持することになるため、ラムダを使用するよりも悪いです。

  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 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート