Programing

ThreadLocals를 정리하는 방법

crosscheck 2021. 1. 6. 20:17
반응형

ThreadLocals를 정리하는 방법


아무도 이것을 수행하는 방법에 대한 예가 있습니까? 가비지 수집기에서 처리합니까? Tomcat 6을 사용하고 있습니다.


javadoc은 다음과 같이 말합니다.

"각 스레드는 스레드가 활성 상태이고 ThreadLocal 인스턴스에 액세스 할 수있는 한 스레드 로컬 변수의 복사본에 대한 암시 적 참조를 보유합니다. 스레드가 사라지면 스레드 로컬 인스턴스의 모든 복사본이 가비지 수집의 대상이됩니다. (이 사본에 대한 다른 참조가 존재하지 않는 한).

응용 프로그램 또는 (요청 스레드에 대해 이야기하는 경우) 컨테이너가 스레드 풀을 사용하는 경우 스레드가 죽지 않습니다. 필요한 경우 스레드 로컬을 직접 처리해야합니다. 이를 수행하는 유일한 방법은 ThreadLocal.remove()메서드 를 호출하는 것입니다.

스레드 풀의 스레드에 대한 스레드 로컬을 정리하려는 두 가지 이유가 있습니다.

  • 메모리 (또는 가상 리소스) 누수 방지
  • 스레드 로컬을 통해 한 요청에서 다른 요청으로 실수로 정보가 유출되는 것을 방지합니다.

스레드 로컬 메모리 누수는 모든 스레드 로컬이 결국 덮어 쓰기 될 가능성이 있으므로 일반적으로 제한된 스레드 풀의 주요 문제가되지 않아야합니다. 즉 스레드가 재사용 될 때. 그러나 변수를 ThreadLocal사용하여 static싱글 톤 인스턴스를 유지하는 대신 인스턴스를 반복해서 생성하는 실수를 하면 스레드 로컬 값이 덮어 쓰여지지 않고 각 스레드의 threadlocals맵에 누적 됩니다. 이로 인해 심각한 누출이 발생할 수 있습니다.


웹앱에서 HTTP 요청을 처리하는 동안 생성 / 사용되는 스레드 로컬에 대해 이야기하고 있다고 가정하면 스레드 로컬 누수를 방지하는 한 가지 방법은 a ServletRequestListener를 웹앱 에 등록 ServletContext하고 리스너의 requestDestroyed메서드를 구현 하여 스레드 로컬을 정리하는 것입니다. 현재 스레드.

이 컨텍스트 에서는 한 요청에서 다른 요청으로 정보가 유출 될 가능성도 고려해야합니다 .


다음은 실제 스레드 로컬 변수에 대한 참조가 없을 때 현재 스레드에서 모든 스레드 로컬 변수를 정리하는 코드입니다. 다른 스레드에 대한 스레드 로컬 변수를 정리하기 위해 일반화 할 수도 있습니다.

    private void cleanThreadLocals() {
        try {
            // Get a reference to the thread locals table of the current thread
            Thread thread = Thread.currentThread();
            Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            Object threadLocalTable = threadLocalsField.get(thread);

            // Get a reference to the array holding the thread local variables inside the
            // ThreadLocalMap of the current thread
            Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
            Field tableField = threadLocalMapClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object table = tableField.get(threadLocalTable);

            // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
            // is a reference to the actual ThreadLocal variable
            Field referentField = Reference.class.getDeclaredField("referent");
            referentField.setAccessible(true);

            for (int i=0; i < Array.getLength(table); i++) {
                // Each entry in the table array of ThreadLocalMap is an Entry object
                // representing the thread local reference and its value
                Object entry = Array.get(table, i);
                if (entry != null) {
                    // Get a reference to the thread local object and remove it from the table
                    ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
                    threadLocal.remove();
                }
            }
        } catch(Exception e) {
            // We will tolerate an exception here and just log it
            throw new IllegalStateException(e);
        }
    }

ThreadLocal값을 처음에 넣은 스레드 내부 (또는 스레드가 가비지 수집 될 때-작업자 스레드의 경우가 아님)를 제외하고는 값 을 정리할 방법이 없습니다 . 이는 서블릿 요청이 완료 될 때 (또는 AsyncContext를 Servlet 3의 다른 스레드로 전송하기 전에) ThreadLocal을 정리하는 데주의를 기울여야 함을 의미합니다. 그 시점 이후에는 특정 작업자 스레드에 들어갈 기회를 얻지 못할 수 있으므로, 서버가 다시 시작되지 않는 동안 웹 앱이 배포 해제 된 상황에서 메모리가 누수됩니다.

이러한 정리를 수행하기에 좋은 곳은 ServletRequestListener.requestDestroyed () 입니다.

Spring을 사용하는 경우 필요한 모든 배선이 이미 준비되어 있으므로 정리에 대한 걱정없이 간단히 요청 범위에 항목을 넣을 수 있습니다 (자동으로 수행됨).

RequestContextHolder.getRequestAttributes().setAttribute("myAttr", myAttr, RequestAttributes.SCOPE_REQUEST);
. . .
RequestContextHolder.getRequestAttributes().getAttribute("myAttr", RequestAttributes.SCOPE_REQUEST);

Javadoc 문서를주의 깊게 다시 읽으십시오.

'각 스레드는 스레드가 살아 있고 ThreadLocal 인스턴스에 액세스 할 수있는 한 스레드 로컬 변수의 복사본에 대한 암시 적 참조를 보유합니다. 스레드가 사라진 후 스레드 로컬 인스턴스의 모든 복사본은 가비지 컬렉션의 대상이됩니다 (이러한 복사본에 대한 다른 참조가 존재하지 않는 한). '

아무것도 청소할 필요가 없으며 누출이 살아 남기위한 'AND'조건이 있습니다. 따라서 스레드가 응용 프로그램에 대해 살아남는 웹 컨테이너에서도 webapp 클래스가 언로드되는 한 (부모 클래스 로더에로드 된 정적 클래스의 beeing 참조 만이를 방지 할 수 있으며 이것은 ThreadLocal과 관련이 없지만 일반적인 문제는 정적 데이터가있는 공유 jar) AND 조건의 두 번째 구간이 더 이상 충족되지 않으므로 스레드 로컬 복사본이 가비지 수집에 적합합니다.

구현이 문서를 충족하는 한 스레드 로컬은 메모리 누수의 원인이 될 수 없습니다.


이 질문이 오래되었지만이 질문에 대한 답변을 제공하고 싶습니다. 나는 같은 문제 (gson threadlocal이 요청 스레드에서 제거되지 않음)에 시달렸으며 메모리가 부족할 때마다 서버를 다시 시작하는 것이 편안했습니다 (큰 시간을 씁니다!).

개발 모드로 설정된 자바 웹 앱의 맥락에서 (서버가 코드 변경을 감지 할 때마다 바운스하도록 설정되고 디버그 모드에서도 실행될 수 있음) 스레드 로컬이 멋질 수 있음을 빠르게 배웠습니다. 때로는 고통 스럽습니다. 모든 요청에 ​​대해 threadlocal 호출을 사용했습니다. 호출 내부. 나는 때때로 gson을 사용하여 내 응답을 생성합니다. 필터의 'try'블록 안에 Invocation을 래핑하고 'finally'블록 내부에서 파괴합니다.

내가 관찰 한 것은 (지금은 이것을 백업 할 메트릭이 없음) 여러 파일을 변경하고 서버가 변경 사항 사이에 지속적으로 튀어 오르면 참을성이 없어서 서버를 다시 시작한다는 것입니다 (정확하게 말하면 톰캣). IDE에서. 대부분의 경우 '메모리 부족'예외로 끝날 것입니다.

이 문제를 해결하는 방법은 내 앱에 ServletRequestListener 구현을 포함하는 것이었고 문제는 사라졌습니다. 나는 요청 도중에 서버가 여러 번 바운스되면 내 threadlocals가 정리되지 않았으므로 (gson 포함) threadlocals에 대한이 경고를 받고 나중에 두세 번 경고를 받게 될 것이라고 생각합니다. 서버가 충돌합니다. ServletResponseListener가 명시 적으로 내 threadlocals를 닫으면 gson 문제가 사라졌습니다.

이것이 의미가 있고 스레드 로컬 문제를 극복하는 방법에 대한 아이디어를 제공하기를 바랍니다. 항상 사용 지점 주변에서 닫으십시오. ServletRequestListener에서 각 threadlocal 래퍼를 테스트하고 여전히 일부 개체에 대한 유효한 참조가있는 경우 해당 지점에서 삭제합니다.

또한 threadlocal을 클래스 내부의 정적 변수로 래핑하는 습관을 들인다는 점도 지적해야합니다. 이렇게하면 ServeltRequestListener에서 삭제함으로써 동일한 클래스의 다른 인스턴스가 어슬렁 거리는 것에 대해 걱정할 필요가 없다는 것을 보장 할 수 있습니다.


JVM은 ThreadLocal 개체 내에있는 모든 참조없는 개체를 자동으로 정리합니다.

이러한 객체를 정리하는 또 다른 방법 (예를 들어, 이러한 객체는 주변에 존재하는 모든 스레드 안전하지 않은 객체 일 수 있음)은 기본적으로 보유하고있는 객체 홀더 클래스에 배치하는 것입니다.이 클래스는 객체를 정리하기 위해 finalize 메서드를 재정의 할 수 있습니다. 그 안에 상주합니다. 다시 말하지만 가비지 콜렉터와 해당 정책에 따라 finalize메서드를 호출 할 수 있습니다.

다음은 코드 샘플입니다.

public class MyObjectHolder {

    private MyObject myObject;

    public MyObjectHolder(MyObject myObj) {
        myObject = myObj;
    }

    public MyObject getMyObject() {
        return myObject;
    }

    protected void finalize() throws Throwable {
        myObject.cleanItUp();
    }
}

public class SomeOtherClass {
    static ThreadLocal<MyObjectHolder> threadLocal = new ThreadLocal<MyObjectHolder>();
    .
    .
    .
}

@lyaffe의 답변은 Java 6에서 가능한 가장 좋습니다.이 답변은 Java 8에서 사용할 수있는 것을 사용하여 해결하는 몇 가지 문제가 있습니다.

@lyaffe의 답변은 Java 6 용으로 작성 MethodHandle되었습니다. 반사로 인해 성능이 저하됩니다. 아래와 같이 사용 하면 필드 및 메서드에 MethodHandle대한 제로 오버 헤드 액세스를 제공 합니다.

@lyaffe의 대답은 ThreadLocalMap.table명시 적으로 진행 되며 버그가 발생하기 쉽습니다. ThreadLocalMap.expungeStaleEntries()동일한 작업을 수행 할 수 있는 방법이 있습니다 .

아래 코드에는 호출 비용을 최소화하기위한 3 가지 초기화 메서드가 expungeStaleEntries()있습니다.

private static final MethodHandle        s_getThreadLocals     = initThreadLocals();
private static final MethodHandle        s_expungeStaleEntries = initExpungeStaleEntries();
private static final ThreadLocal<Object> s_threadLocals        = ThreadLocal.withInitial(() -> getThreadLocals());

public static void expungeThreadLocalMap()
{
   Object threadLocals;

   threadLocals = s_threadLocals.get();

   try
   {
      s_expungeStaleEntries.invoke(threadLocals);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }
}

private static Object getThreadLocals()
{
   ThreadLocal<Object> local;
   Object result;
   Thread thread;

   local = new ThreadLocal<>();

   local.set(local);   // Force ThreadLocal to initialize Thread.threadLocals

   thread = Thread.currentThread();

   try
   {
      result = s_getThreadLocals.invoke(thread);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }

   return(result);
}

private static MethodHandle initThreadLocals()
{
   MethodHandle result;
   Field field;

   try
   {
      field = Thread.class.getDeclaredField("threadLocals");

      field.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflectGetter(field);

      result = Preconditions.verifyNotNull(result, "result is null");
   }
   catch (NoSuchFieldException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}

private static MethodHandle initExpungeStaleEntries()
{
   MethodHandle result;
   Class<?> clazz;
   Method method;
   Object threadLocals;

   threadLocals = getThreadLocals();
   clazz        = threadLocals.getClass();

   try
   {
      method = clazz.getDeclaredMethod("expungeStaleEntries");

      method.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflect(method);
   }
   catch (NoSuchMethodException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}

참조 URL : https://stackoverflow.com/questions/3869026/how-to-clean-up-threadlocals

반응형