ThreadLocal变量的性能


86

ThreadLocal变量读取的速度比从常规字段读取的速度慢多少?

更具体地说,简单对象创建比访问ThreadLocal变量快还是慢?

我认为它足够快,因此ThreadLocal<MessageDigest>MessageDigest每次创建实例相比,拥有实例要快得多。但这是否也适用于字节[10]或字节[1000]?

编辑:问题是调用ThreadLocalget时真正发生了什么?如果那只是一个领域,就像其他领域一样,那么答案将是“它总是最快的”,对吗?


2
本地线程基本上是一个包含哈希图和查找的字段,其中键是当前线程对象。因此,它慢得多,但仍然快。:)
eckes

1
@eckes:它的行为当然是这样的,但是通常不是这样实现的。而是Thread包含一个(不同步的)哈希图,其中的键是当前ThreadLocal对象
sbk

Answers:


40

运行未发布的基准测试,ThreadLocal.get我的计算机上每次迭代大约需要35个周期。没什么。在Sun的实现中,ThreadThreadLocals映射为值的自定义线性探测哈希图。由于只能通过单个线程访问它,因此它可能非常快。

小对象的分配需要类似的周期数,尽管由于缓存耗尽,您可能会在紧密循环中得到较低的数字。

的建造MessageDigest可能相对昂贵。它具有相当数量的状态,并且通过ProviderSPI机制进行构造。您可以通过克隆或提供来进行优化Provider

仅仅因为缓存ThreadLocal而不是创建缓存可能更快,并不一定意味着系统性能会提高。您将有与GC相关的额外开销,这会降低一切。

除非您的应用程序使用率很高,否则您MessageDigest可能需要考虑使用常规的线程安全缓存。


5
恕我直言,最快的方法就是忽略SPI并使用new org.bouncycastle.crypto.digests.SHA1Digest()。我很确定没有缓存能胜过它。
maaartinus 2011年

57

在2009年,一些JVM使用Thread.currentThread()对象中的未同步HashMap实现了ThreadLocal。这使其速度非常快(尽管当然不及使用常规字段访问的速度快),并且确保Thread死亡时对ThreadLocal对象进行了整理。在2016年更新此答案时,似乎大多数(全部?)较新的JVM使用带有线性探测的ThreadLocalMap。我不确定这些服务器的性能如何,但是我无法想象它比早期的实施要差得多。

当然,如今新的Object()也非常快,垃圾回收器也非常擅长回收短命的对象。

除非您确定对象创建将是昂贵的,或者需要逐个线程地保持某些状态,否则最好在需要的解决方案上使用更简单的分配方法,而仅当出现以下情况时才切换到ThreadLocal实现:分析器告诉您您需要。


4
+1是实际解决该问题的唯一答案。
cletus

您能给我一个不对ThreadLocalMap使用线性探测的现代JVM的例子吗?Java 8 OpenJDK似乎仍在使用ThreadLocalMap和线性探测。grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…– Karthick
'16

1
@Karthick对不起,我不能。我在2009年写了这篇文章。我将进行更新。
比尔·米歇尔

34

好问题,最近我一直在问自己。为了给您确定的数字,下面的基准(在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.8 GHz双核和具有超线程(2.67 GHz)的四核i7。

这些是数字:

i7

规格:Intel i7 2x四核@ 2.67 GHz测试: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.8 GHz测试: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实现以及具有处理器数量的这些体系结构上,它似乎也可以很好地扩展。


5
+1荣誉是唯一能给出定量结果的方法。我有点怀疑,因为这些测试是在Scala中进行的,但是就像您说的那样,Java字节码应该类似...
Gravity

谢谢!这个while循环实际上产生与相应Java代码相同的字节码。但是,在不同的VM上可以观察到不同的时间-已经在Sun JVM1.6上进行了测试。
axel22 2011年

此基准代码无法模拟ThreadLocal的良好用例。在第一种方法中:每个线程在内存中都有一个共享的表示形式,字符串不变。在第二种方法中,您对哈希表查找的成本进行了基准测试,其中字符串在所有线程之间是分离的。
Joelmob

字符串不会改变,但是"!"会在第一种方法中从内存中读取(永远不会进行写操作)-第一种方法实际上等效于子类化Thread并为其提供一个自定义字段。该基准测试测量了一种极端情况,其中整个计算包括读取局部变量/线程-实际应用程序可能不会受到其访问模式的影响,但在最坏的情况下,它们的行为将如上所述。
axel22

4

在这里,它进行了另一项测试。结果表明,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尾字段样本:6044

0运行线程本地样本

0端线程本地样本:6015

1-运行现场样本

1-End字段样本:5095

1-运行线程本地样本

1-End线程本地样本:5720

2-运行现场样本

2端字段样本:4842

2-运行线程本地样本

2端线程本地样本:5835

3-运行现场样本

三端场样本:4674

3-运行线程本地样本

3端螺纹局部样本:5287

4-运行现场样本

4端野外采样:4849

4-运行线程本地样本

4端线程本地样本:5309

5运行领域样本

5端字段样本:4781

5-运行线程本地样本

5端螺纹局部样本:5330

6运行现场样本

6端字段样本:5294

6运行线程本地样本

6端线程本地样本:5511

7运行字段样本

7端字段样本:5119

7运行线程本地样本

7端线程本地样本:5793

8-运行现场样本

8端野外采样:4977

8-运行线程本地样本

8端线程本地样本:6374

9运行字段样本

9端字段样本:4841

9运行线程本地样本

9端线程本地样本:5471

场平均:5051

线程本地平均:5664

信封:

openjdk版本“ 1.8.0_131”

英特尔®酷睿™i7-7500U CPU @ 2.70GHz×4

Ubuntu 16.04 LTS


抱歉,这甚至还不是有效的测试。A)最大的问题:您在每次迭代中分配String(Int.toString)与测试相比,这是非常昂贵的。B)您在每次迭代中都要进行两次地图操作,这也是完全无关且昂贵的。尝试改为从ThreadLocal增加一个原始int。C)使用System.nanoTime代替System.currentTimeMillis,前者用于配置文件,后者用于用户日期时间目的,可以根据需要进行更改。D)您应该完全避免分配,包括“示例”类的顶级分配
Philip Guin

3

在您优化之前,@ Pete是正确的测试。

如果与实际使用它相比,构造一个MessageDigest有任何严重的开销,我将感到非常惊讶。

缺少使用ThreadLocal的小姐可能会导致泄漏和悬而未决的引用,它们没有明确的生命周期,通常我永远不会在没有非常明确的计划何时删除特定资源的情况下使用ThreadLocal。


0

构建它并对其进行度量。

另外,如果将消息摘要行为封装到一个对象中,则只需要一个threadlocal。如果出于某些目的需要本地MessageDigest和本地byte [1000],请创建一个具有messageDigest和byte []字段的对象,然后将该对象放入ThreadLocal中,而不是将二者分别放置。


谢谢,MessageDigest和byte []的用法不同,因此不需要一个对象。
萨蒙2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.