[java] ThreadLocal 변수의 성능

ThreadLocal일반 필드보다 느린 변수 에서 얼마나 많이 읽 습니까?

더 구체적으로 간단한 객체 생성이 ThreadLocal변수에 대한 액세스보다 빠르거나 느린 가요?

ThreadLocal<MessageDigest>인스턴스 를 갖는 것이 MessageDigest매번 인스턴스를 만드는 것보다 훨씬 빠를 정도로 충분히 빠르다고 가정합니다 . 하지만 예를 들어 byte [10] 또는 byte [1000]에도 적용됩니까?

편집 : 질문은 ThreadLocal‘s get을 호출 할 때 실제로 무슨 일이 일어나고 있습니까? 그게 다른 분야와 마찬가지로 그저 필드라면 대답은 “항상 가장 빠르다”입니다. 맞죠?



답변

게시되지 않은 벤치 마크를 실행 ThreadLocal.get하면 내 컴퓨터에서 반복 당 약 35주기가 걸립니다. 그다지 중요하지 않습니다. Sun의 구현에서 사용자 정의 선형 프로빙 해시 맵은 s를 값 으로 Thread매핑 ThreadLocal합니다. 단일 스레드에서만 액세스 할 수 있기 때문에 매우 빠를 수 있습니다.

작은 개체의 할당은 비슷한 수의주기를 필요로하지만 캐시 고갈로 인해 타이트한 루프에서 다소 낮은 수치를 얻을 수 있습니다.

의 건설은 MessageDigest상대적으로 비용이 많이 듭니다. 그것은 상당한 양의 상태를 가지고 있으며 ProviderSPI 메커니즘을 통과합니다 . 예를 들어 .NET Framework를 복제하거나 제공하여 최적화 할 수 있습니다 Provider.

ThreadLocal생성 하는 것보다 캐시하는 것이 더 빠르다고해서 반드시 시스템 성능이 향상되는 것은 아닙니다. 모든 것을 느리게하는 GC와 관련된 추가 오버 헤드가 있습니다.

응용 프로그램이 매우 많이 사용하지 않는 MessageDigest한 기존의 스레드 안전 캐시를 대신 사용하는 것이 좋습니다.


답변

2009 년에 일부 JVM은 Thread.currentThread () 객체에서 동기화되지 않은 HashMap을 사용하여 ThreadLocal을 구현했습니다. 이로 인해 (물론 일반 필드 액세스를 사용하는 것만 큼 빠르지는 않지만) 매우 빠르며 ThreadLocal 객체가 Thread가 죽었을 때 정리되었습니다. 2016 년 에이 답변을 업데이트하면 대부분의 (모두?) 최신 JVM이 선형 프로브와 함께 ThreadLocalMap을 사용하는 것 같습니다. 나는 그것들의 성능에 대해 확신하지 못합니다. 그러나 이것이 이전 구현보다 훨씬 더 나쁘다는 것을 상상할 수 없습니다.

물론 최근에는 new Object ()도 매우 빠르며 가비지 콜렉터도 수명이 짧은 객체를 회수하는 데 매우 능숙합니다.

객체 생성에 비용이 많이들 것이라는 확신이 없거나 스레드 단위로 스레드에서 일부 상태를 유지해야하는 경우가 아니면 필요한 경우 더 간단한 할당 솔루션을 사용하고 다음과 같은 경우에만 ThreadLocal 구현으로 전환하는 것이 좋습니다. 프로파일 러가 필요하다고 알려줍니다.


답변

좋은 질문입니다. 최근에 스스로에게 물어 보았습니다. 명확한 숫자를 제공하기 위해 아래 벤치 마크 (Scala에서 동등한 Java 코드와 거의 동일한 바이트 코드로 컴파일 됨) :

var cnt: String = ""
val tlocal = new java.lang.ThreadLocal[String] {
  override def initialValue = ""
}

def loop_heap_write = {
  var i = 0
  val until = totalwork / threadnum                                                                                                               
  while (i < until) {
    if (cnt ne "") cnt = "!"
    i += 1
  }
  cnt
}

def threadlocal = {
  var i = 0
  val until = totalwork / threadnum
  while (i < until) {
    if (tlocal.get eq null) i = until + i + 1
    i += 1
  }
  if (i > until) println("thread local value was null " + i)
}

여기 에서 사용 가능 하며 AMD 4x 2.8GHz 듀얼 코어 및 하이퍼 스레딩 (2.67GHz)이있는 쿼드 코어 i7에서 수행되었습니다.

다음은 숫자입니다.

i7

사양 : Intel i7 2x 쿼드 코어 @ 2.67GHz 테스트 : scala.threads.ParallelTests

테스트 이름 : loop_heap_read

스레드 수 : 1 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 9.0069 9.0036 9.0017 9.0084 9.0074 (평균 = 9.1034 최소 = 8.9986 최대 = 21.0306)

스레드 수 : 2 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 4.5563 4.7128 4.5663 4.5617 4.5724 (평균 = 4.6337 최소 = 4.5509 최대 = 13.9476)

스레드 수 : 4 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 2.3946 2.3979 2.3934 2.3937 2.3964 (평균 = 2.5113 최소 = 2.3884 최대 = 13.5496)

스레드 수 : 8 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 2.4479 2.4362 2.4323 2.4472 2.4383 (평균 = 2.5562 최소 = 2.4166 최대 = 10.3726)

테스트 이름 : threadlocal

스레드 수 : 1 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 91.1741 90.8978 90.6181 90.6200 90.6113 (평균 = 91.0291 최소 = 90.6000 최대 = 129.7501)

스레드 수 : 2 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 45.3838 45.3858 45.6676 45.3772 45.3839 (평균 = 46.0555 최소 = 45.3726 최대 = 90.7108)

스레드 수 : 4 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 22.8118 22.8135 59.1753 22.8229 22.8172 (평균 = 23.9752 최소 = 22.7951 최대 = 59.1753)

스레드 수 : 8 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 22.2965 22.2415 22.3438 22.3109 22.4460 (평균 = 23.2676 최소 = 22.2346 최대 = 50.3583)

AMD

사양 : AMD 8220 4x 듀얼 코어 @ 2.8GHz 테스트 : scala.threads.ParallelTests

테스트 이름 : loop_heap_read

총 작업 : 20000000 스레드 수 : 1 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 12.625 12.631 12.634 12.632 12.628 (평균 = 12.7333 분 = 12.619 최대 = 26.698)

테스트 이름 : loop_heap_read 총 작업 : 20000000

실행 시간 : (마지막 5 개 표시) 6.412 6.424 6.408 6.397 6.43 (평균 = 6.5367 최소 = 6.393 최대 = 19.716)

스레드 수 : 4 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 3.385 4.298 9.7 6.535 3.385 (평균 = 5.6079 최소 = 3.354 최대 = 21.603)

스레드 수 : 8 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 5.389 5.795 10.818 3.823 3.824 (평균 = 5.5810 최소 = 2.405 최대 = 19.755)

테스트 이름 : threadlocal

스레드 수 : 1 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 200.217 207.335 200.241 207.342 200.23 (평균 = 202.2424 분 = 200.184 최대 = 245.369)

스레드 수 : 2 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 100.208 100.199 100.211 103.781 100.215 (평균 = 102.2238 최소 = 100.192 최대 = 129.505)

스레드 수 : 4 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 62.101 67.629 62.087 52.021 55.766 (평균 = 65.6361 최소 = 50.282 최대 = 167.433)

스레드 수 : 8 총 테스트 : 200

실행 시간 : (마지막 5 개 표시) 40.672 74.301 34.434 41.549 28.119 (평균 = 54.7701 분 = 28.119 최대 = 94.424)

요약

스레드 로컬은 힙 읽기의 약 10-20 배입니다. 또한이 JVM 구현과 프로세서 수에 따라 이러한 아키텍처에서 잘 확장되는 것으로 보입니다.


답변

여기에 또 다른 테스트가 있습니다. 결과는 ThreadLocal이 일반 필드보다 약간 느리지 만 순서는 동일하다는 것을 보여줍니다. Aprox 12 % 느림

public class Test {
private static final int N = 100000000;
private static int fieldExecTime = 0;
private static int threadLocalExecTime = 0;

public static void main(String[] args) throws InterruptedException {
    int execs = 10;
    for (int i = 0; i < execs; i++) {
        new FieldExample().run(i);
        new ThreadLocaldExample().run(i);
    }
    System.out.println("Field avg:"+(fieldExecTime / execs));
    System.out.println("ThreadLocal avg:"+(threadLocalExecTime / execs));
}

private static class FieldExample {
    private Map<String,String> map = new HashMap<String, String>();

    public void run(int z) {
        System.out.println(z+"-Running  field sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            map.put(s,"a");
            map.remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        fieldExecTime += t;
        System.out.println(z+"-End field sample:"+t);
    }
}

private static class ThreadLocaldExample{
    private ThreadLocal<Map<String,String>> myThreadLocal = new ThreadLocal<Map<String,String>>() {
        @Override protected Map<String, String> initialValue() {
            return new HashMap<String, String>();
        }
    };

    public void run(int z) {
        System.out.println(z+"-Running thread local sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            myThreadLocal.get().put(s, "a");
            myThreadLocal.get().remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        threadLocalExecTime += t;
        System.out.println(z+"-End thread local sample:"+t);
    }
}
}'

산출:

0- 실행 필드 샘플

0-End 필드 샘플 : 6044

0- 실행 스레드 로컬 샘플

0-End 스레드 로컬 샘플 : 6015

1- 러닝 필드 샘플

1-End 필드 샘플 : 5095

1- 실행 스레드 로컬 샘플

1- 엔드 스레드 로컬 샘플 : 5720

2- 러닝 필드 샘플

2-End 필드 샘플 : 4842

2- 실행 스레드 로컬 샘플

2- 엔드 스레드 로컬 샘플 : 5835

3- 러닝 필드 샘플

3-End 필드 샘플 : 4674

3- 실행 스레드 로컬 샘플

3- 엔드 스레드 로컬 샘플 : 5287

4- 러닝 필드 샘플

4- 엔드 필드 샘플 : 4849

4- 실행 스레드 로컬 샘플

4- 엔드 스레드 로컬 샘플 : 5309

5- 러닝 필드 샘플

5-End 필드 샘플 : 4781

5- 실행 스레드 로컬 샘플

5- 엔드 스레드 로컬 샘플 : 5330

6- 러닝 필드 샘플

6-End 필드 샘플 : 5294

6- 실행 스레드 로컬 샘플

6- 엔드 스레드 로컬 샘플 : 5511

7- 실행 필드 샘플

7-End 필드 샘플 : 5119

7- 실행 스레드 로컬 샘플

7-End 스레드 로컬 샘플 : 5793

8- 실행 필드 샘플

8-End 필드 샘플 : 4977

8- 실행 스레드 로컬 샘플

8- 엔드 스레드 로컬 샘플 : 6374

9- 러닝 필드 샘플

9-End 필드 샘플 : 4841

9- 실행 스레드 로컬 샘플

9- 엔드 스레드 로컬 샘플 : 5471

필드 평균 : 5051

ThreadLocal 평균 : 5664

환경 :

openjdk 버전 “1.8.0_131”

Intel® Core ™ i7-7500U CPU @ 2.70GHz × 4

Ubuntu 16.04 LTS


답변

@Pete는 최적화하기 전에 올바른 테스트입니다.

MessageDigest를 구성 할 때 실제로 사용하는 것과 비교할 때 심각한 오버 헤드가 있으면 매우 놀랍습니다.

ThreadLocal을 사용하지 않는 것은 명확한 수명주기가없는 누수 및 매달려있는 참조의 원인이 될 수 있습니다. 일반적으로 특정 리소스가 제거 될시기에 대한 명확한 계획 없이는 ThreadLocal을 사용하지 않습니다.


답변

그것을 구축하고 측정하십시오.

또한 메시지 요약 동작을 객체로 캡슐화하는 경우 하나의 threadlocal 만 필요합니다. 어떤 목적으로 로컬 MessageDigest와 로컬 byte [1000]가 필요한 경우 messageDigest 및 byte [] 필드가있는 개체를 만들고 해당 개체를 개별적으로 두지 않고 ThreadLocal에 넣습니다.


답변