> Java > java지도 시간 > 본문

Java Cleaners: 외부 리소스를 관리하는 현대적인 방법

PHPz
풀어 주다: 2024-08-14 12:40:32
원래의
595명이 탐색했습니다.

이 기사의 코드는 GitHub에서 찾을 수 있습니다.
예제를 보기 전에 내부적으로 어떻게 작동하는지 이해하고 싶은 프로그래머라면
소개 후 현장 뒤편의 클리너로 바로 이동할 수 있습니다.

  • 소개
  • 간단한 클리너 작동
  • 청소부, 올바른 길
  • 청소기, 효과적인 방법
  • 청소부 뒷모습

소개

외부 리소스(파일, 소켓 등)에 대한 참조를 보유하는 개체가 있는 시나리오를 생각해 보세요. 그리고 보유 객체가 더 이상 활성/액세스 가능하지 않을 때 이러한 리소스가 해제되는 방법을 제어하고 싶습니다. Java에서 이를 어떻게 달성합니까? Java 9 이전에는 프로그래머가 객체 클래스 finalize() 메서드를 재정의하여 종료자를 사용할 수 있었습니다. Finalizer에는 느리고, 신뢰할 수 없으며, 위험한 등 많은 단점이 있습니다. JDK를 구현하는 사람이나 사용하는 사람 모두가 싫어하는 기능 중 하나입니다.

Java 9부터 Finalizer는 더 이상 사용되지 않으며 프로그래머는 Cleaners에서 이를 달성할 수 있는 더 나은 옵션을 갖게 되었으며 Cleaners는 정리/마무리 작업을 관리하고 처리하는 더 나은 방법을 제공합니다. 클리너는 리소스 보유 개체가 자신과 해당 정리 작업을 등록하도록 하는 패턴으로 작동합니다. 그런 다음 애플리케이션 코드에서 해당 객체에 액세스할 수 없으면 Cleaners가 정리 작업을 호출합니다.
이것은 Cleaner가 Finalizer보다 나은 이유를 설명하는 기사는 아니지만 몇 가지 차이점을 간략하게 나열하겠습니다.

파이널라이저 대 클리너

종료자 청소기 종료자는 Garbage Collector의 스레드 중 하나에 의해 호출됩니다. 프로그래머는 어떤 스레드가 최종화 논리를 호출할지 제어할 수 없습니다. 파이널라이저와 달리 클리너를 사용하면 프로그래머는 정리 논리를 호출하는 스레드를 제어할 수 있습니다. 객체가 실제로 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
Phantom Reachable이 되면 정리 로직이 호출됩니다. 즉, 애플리케이션이 더 이상 해당 객체에 액세스할 수 없습니다. 완료 로직은 리소스를 보유하는 객체의 일부입니다 청소 로직과 해당 상태는 별도의 개체에 캡슐화됩니다. 등록/등록 취소 메커니즘 없음 정리 작업 등록 및 명시적 호출/등록 취소를 위한 수단 제공

간단한 클리너 실행

충분한 잡담을 통해 청소부들의 활약을 지켜보세요.

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();
        }}
로그인 후 복사

몇 줄의 코드지만 여기서는 많은 일이 일어나고 있습니다. 분석해 보겠습니다

  1. CLEANER 상수는 이름에서 알 수 있듯이 java.lang.ref.Cleaner 유형입니다. 이것이 Java Cleaners 기능의 중심이자 시작점입니다. CLEANER 변수는 정적으로 선언되어야 합니다. Cleaner는 인스턴스 변수가 되어서는 안 됩니다. 가능한 한 여러 학급에서 공유해야 합니다.
  2. 생성자에서 ResourceHolder의 인스턴스는 정리 작업과 함께 Cleaner에 등록됩니다. 정리 작업은 Cleaner가 최대 한 번(최대 한 번, 즉 실행하지 않을 수 있음을 의미) 호출을 보장하는 실행 가능한 작업입니다. 조금도). Cleaner의 Register() 메소드를 호출함으로써 이러한 인스턴스는 기본적으로 Cleaner에 두 가지 내용을 전달합니다.
    • 내가 살아있는 동안 나를 추적하세요
    • 그리고 제가 더 이상 활동하지 않으면(Phantom Reachable) 최선을 다해 청소 작업을 호출해 주세요.
  3. 기본 메소드에서는 ResourceHolder의 객체를 인스턴스화하고 해당 변수를 즉시 null로 설정합니다. 객체에 변수 참조가 하나만 있기 때문에 애플리케이션은 더 이상 객체에 액세스할 수 없습니다. 즉, 이 됩니다. 팬텀 도달 가능
  4. System.gc()를 호출하여 JVM에 가비지 컬렉터 실행을 요청합니다. 결과적으로 Cleaner가 청소 작업을 실행하게 됩니다. 일반적으로 System.gc()를 호출할 필요는 없지만 애플리케이션만큼 간단합니다. 작업을 실행하려면 Cleaner를 활성화해야 합니다

애플리케이션을 실행하면 표준 출력 어딘가를 정리하고 있다는 것을 알 수 있기를 바랍니다.

? 주의
클리너를 사용하는 가장 간단한 방법부터 시작하여 단순화된 방식으로 사용법을 보여드릴 수 있습니다. 하지만 이는 클리너를 사용하는 효과적인 방법도 올바른 방법도 아니라는 점을 명심하세요

청소부, 올바른 방법

첫 번째 예는 Cleaners가 실제로 작동하는 모습을 볼 수 있을 만큼 훌륭했습니다.
하지만 경고했듯이 실제 애플리케이션에서 Cleaner를 사용하는 올바른 방법은 아닙니다.
우리가 한 일에 어떤 문제가 있는지 살펴보겠습니다.

  1. ResourceHolder의 클래스 멤버로 Cleaner 객체를 시작했습니다. 앞서 언급했듯이 Cleaner는 클래스 간에 공유되어야 하며 개별 클래스에 속해서는 안 됩니다. 각 Cleaner 인스턴스가 존재하는 이유는 제한된 기본 리소스인 스레드를 유지 관리하기 때문입니다. , 네이티브 리소스를 소비할 때는 주의해야 합니다.
    실제 애플리케이션에서는 일반적으로
    와 같은 싱글톤 클래스나 유틸리티에서 Cleaner 객체를 얻습니다.

      private static CLEANER = AppUtil.getCleaner();
    
    로그인 후 복사
  2. 청소 작업으로 람다를 전달했습니다. 청소 작업으로 람다를 절대 전달하면 안 됩니다.
    그 이유를 이해하려면
    인쇄된 메시지를 추출하여 이전 예제를 리팩터링하고 이를 인스턴스 변수로 만들어 보겠습니다

    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));
       }
    }
    
    로그인 후 복사

    애플리케이션을 실행하고 어떤 일이 일어나는지 확인하세요.
    무슨 일이 일어나는지 알려드릴게요
    애플리케이션을 몇 번이나 실행하더라도 정리 작업은 호출되지 않습니다.
    그 이유를 살펴보겠습니다

    • 내부적으로 Cleaner는 PhantomReference 및 ReferenceQueue를 사용하여 등록된 개체를 추적합니다. 객체가 Phantom Reachable이 되면 ReferenceQueue가 Cleaner에 이를 알립니다. Cleaner는 스레드를 사용하여 해당 청소 작업을 실행합니다.
    • 람다가 인스턴스 멤버에 액세스하도록 함으로써 우리는 람다가 이 참조(ResourceHolder 인스턴스의)를 보유하도록 강제합니다. 이로 인해 객체는 결코 Phantom Reachable<🎜이 되지 않습니다. > 우리의 애플리케이션 코드에는 여전히 이에 대한 참조가 있기 때문입니다.

    ? 참고
    첫 번째 예에서 청소 작업이 람다임에도 불구하고 어떻게 호출되는지 궁금하다면. 그 이유는 첫 번째 예의 람다는 어떤 인스턴스 변수에도 액세스하지 않으며 내부 클래스와 달리 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으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿