Maison > Java > javaDidacticiel > le corps du texte

Java Cleaners : la manière moderne de gérer les ressources externes

PHPz
Libérer: 2024-08-14 12:40:32
original
595 Les gens l'ont consulté

Le code de cet article peut être trouvé sur GitHub.
Si vous êtes le genre de programmeur qui aime comprendre le fonctionnement interne des choses avant de voir des exemples,
vous pouvez accéder directement aux nettoyeurs dans les coulisses après l'introduction.

  • Présentation
  • Nettoyant simple en action
  • Nettoyeurs, dans le bon sens
  • Nettoyants, le moyen efficace
  • Nettoyeurs dans les coulisses

Introduction

Pensez à un scénario dans lequel vous disposez d'un objet contenant des références à des ressources externes (fichiers, sockets, etc.). Et vous souhaitez contrôler la façon dont ces ressources sont libérées une fois que l'objet de détention n'est plus actif/accessible, comment y parvenir en Java ?. Avant Java 9, les programmeurs pouvaient utiliser un finaliseur en remplaçant la méthode finalize() de la classe Object. Les finaliseurs présentent de nombreux inconvénients, notamment leur lenteur, leur manque de fiabilité et leur dangerosité. C'est l'une de ces fonctionnalités qui sont détestées à la fois par ceux qui implémentent le JDK et par ceux qui l'utilisent.

Depuis Java 9, les finaliseurs sont obsolètes et les programmeurs disposent d'une meilleure option pour y parvenir dans les nettoyeurs, les nettoyeurs offrent une meilleure façon de gérer et de gérer les actions de nettoyage/finalisation. Les nettoyeurs travaillent selon un modèle dans lequel ils laissent les objets contenant des ressources s'enregistrer eux-mêmes et leurs actions de nettoyage correspondantes. Et puis les Cleaners appelleront les actions de nettoyage une fois que ces objets ne seront plus accessibles par le code de l'application.
Ce n'est pas l'article pour vous expliquer pourquoi les nettoyeurs sont meilleurs que les finaliseurs, même si je vais brièvement énumérer certaines de leurs différences.

Finaliseurs contre nettoyeurs

Finalisateurs Nettoyants ête> Les finaliseurs sont invoqués par l'un des threads de Garbage Collector. En tant que programmeur, vous n'avez aucun contrôle sur le thread qui invoquera votre logique de finalisation Contrairement aux finaliseurs, avec les nettoyeurs, les programmeurs peuvent choisir de contrôler le thread qui invoque la logique de nettoyage. La logique de finalisation est invoquée lorsque l'objet est réellement collecté par GC La logique de nettoyage est invoquée lorsque l'objet devient
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
Phantom Reachable, c'est-à-dire que notre application n'a plus aucun moyen d'y accéder La logique de finalisation fait partie de l'objet contenant les ressources La logique de nettoyage et son état sont encapsulés dans un objet distinct. Aucun mécanisme d'inscription/désinscription Fournit des moyens d'enregistrer les actions de nettoyage et l'appel/désenregistrement explicite

Un nettoyant simple en action

Assez de bavardages, voyons les Nettoyeurs en action.

Détenteur de ressources

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();
        }}
Copier après la connexion

Quelques lignes de code mais il se passe beaucoup de choses ici, décomposons-le

  1. La constante CLEANER est de type java.lang.ref.Cleaner, comme son nom l'indique, c'est le point central et de départ de la fonctionnalité Cleaners en Java. La variable CLEANER est déclarée aussi statique qu'elle devrait l'être, les nettoyeurs ne doivent jamais être des variables d'instance, ils doivent être partagés autant que possible entre différentes classes.
  2. Dans le constructeur, les instances de ResourceHolder s'enregistrent auprès du nettoyeur avec leur action de nettoyage, l'action de nettoyage est une tâche exécutable que le nettoyeur garantit d'invoquer au plus une fois (au plus une fois, ce qui signifie qu'il est possible de ne pas exécuter du tout). En appelant la méthode register() de Cleaner, ces instances disent essentiellement deux choses au Cleaner :
    • Gardez ma trace aussi longtemps que je vivrai
    • Et une fois que je ne serai plus actif (Phantom Reachable), faites de votre mieux et invoquez mon action de nettoyage.
  3. Dans la méthode main, nous instancions un objet de ResourceHolder et définissons immédiatement sa variable sur null, puisque l'objet n'a qu'une seule référence de variable, notre application ne peut plus accéder à l'objet, c'est-à-dire qu'il est devenu Fantôme accessible
  4. Nous appelons System.gc() pour demander à JVM d'exécuter le Garbage Collector, par conséquent, cela déclenchera l’exécution de l’action de nettoyage par le nettoyeur. Généralement, vous n'avez pas besoin d'appeler System.gc() mais aussi simple que notre application, nous devons faciliter le nettoyage pour exécuter l'action

Exécutez l'application et j'espère que vous verrez que je fais un peu de nettoyage quelque part dans votre sortie standard.

? ATTENTION
Nous avons commencé avec la manière la plus simple possible d'utiliser les nettoyants, afin que nous puissions démontrer son utilisation de manière simplifiée, gardez à l'esprit que ce n'est ni efficace ni la bonne façon d'utiliser les nettoyants

Nettoyeurs, la bonne façon

Notre premier exemple était plus que suffisant pour voir Cleaners en action,
mais comme nous l'avons prévenu, ce n'est pas la bonne façon d'utiliser Cleaners dans une application réelle.
Voyons ce qui ne va pas dans ce que nous avons fait.

  1. Nous avons lancé un objet Cleaner en tant que membre de classe du ResourceHolder : comme nous l'avons mentionné précédemment, les nettoyeurs doivent être partagés entre les classes et ne doivent pas appartenir à des classes individuelles, car chaque instance de Cleaner maintient un thread, qui est une ressource native limitée. , et vous voulez être prudent lorsque vous consommez des ressources natives.
    Dans une application réelle, nous obtenons généralement un objet Cleaner à partir d'un utilitaire ou d'une classe Singleton comme

      private static CLEANER = AppUtil.getCleaner();
    
    Copier après la connexion
  2. Nous avons passé un lambda comme action de nettoyage : Vous ne devriez JAMAIS passer un lambda comme action de nettoyage.
    Pour comprendre pourquoi,
    refactorisons notre exemple précédent en extrayant le message imprimé et en faisant une variable d'instance

    Détenteur de ressources

    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));
       }
    }
    
    Copier après la connexion

    Exécutez l'application et voyez ce qui se passe.
    Je vais vous dire ce qui se passe,
    l'action de nettoyage ne sera jamais invoquée, quel que soit le nombre de fois que vous exécutez votre application.
    Voyons pourquoi

    • En interne, les nettoyeurs utilisent PhantomReference et ReferenceQueue pour garder une trace des objets enregistrés, une fois qu'un objet devient Phantom Reachable la ReferenceQueue en informera le nettoyeur et le nettoyeur utilisera son thread pour exécuter l'action de nettoyage correspondante.
    • En permettant au lambda d'accéder au membre de l'instance, nous forçons le lambda à conserver la référence this (de l'instance ResourceHolder), de ce fait, l'objet ne deviendra jamais Phantom Reachable car notre code d'application y fait toujours référence.

    ? REMARQUE
    Si vous vous demandez encore comment, dans notre premier exemple, l'action de nettoyage est invoquée même si elle est un lambda. La raison en est que le lambda du premier exemple n'accède à aucune variable d'instance et, contrairement aux classes internes, les Lambdas ne conserveront pas implicitement la référence de l'objet conteneur à moins d'y être forcés.

    La bonne façon est d'encapsuler votre action de nettoyage avec l'état dont elle a besoin dans une classe imbriquée statique.

    ? Attention
    N'utilisez pas de classe interne anonyme ou non, c'est pire que d'utiliser lambda car une instance de classe interne contiendrait une référence à l'instance de classe externe, qu'elle accède ou non à sa variable d'instance.

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

Copier après la connexion

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


Copier après la connexion

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();
    
    Copier après la connexion

    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;
    
    Copier après la connexion
    Copier après la connexion

    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);
    
    Copier après la connexion

    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;
    
    Copier après la connexion
    Copier après la connexion

    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()
    
    Copier après la connexion

    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

    Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal