ThreadLocal 变量的性能

Posted

技术标签:

【中文标题】ThreadLocal 变量的性能【英文标题】:Performance of ThreadLocal variable 【发布时间】:2010-10-11 05:22:00 【问题描述】:

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

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

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

编辑:问题是调用ThreadLocal's get 时到底发生了什么?如果这只是一个领域,就像任何其他领域一样,那么答案将是“它总是最快的”,对吧?

【问题讨论】:

线程本地基本上是一个包含哈希映射和查找的字段,其中键是当前线程对象。因此,它要慢得多,但仍然很快。 :) @eckes:它的行为当然是这样,但通常不会以这种方式实现。相反,Threads 包含一个(未同步的)哈希图,其中键是当前的 ThreadLocal 对象 【参考方案1】:

构建它并测量它。

此外,如果您将消息摘要行为封装到一个对象中,您只需要一个线程本地。如果出于某种目的需要本地 MessageDigest 和本地 byte[1000],请创建一个带有 messageDigest 和 byte[] 字段的对象,并将该对象放入 ThreadLocal 而不是单独放入。

【讨论】:

谢谢,MessageDigest 和 byte[] 用途不同,所以不需要一个对象。【参考方案2】:

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

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

没有使用 ThreadLocal 可能是泄漏和悬空引用的来源,它们没有明确的生命周期,通常我不会在没有明确计划何时删除特定资源的情况下使用 ThreadLocal。

【讨论】:

【参考方案3】:

在 2009 年,一些 JVM 在 Thread.currentThread() 对象中使用不同步的 HashMap 实现了 ThreadLocal。这使它变得非常快(当然,虽然不如使用常规字段访问快),并确保在 Thread 死亡时整理 ThreadLocal 对象。在 2016 年更新这个答案,似乎大多数(全部?)较新的 JVM 使用带有线性探测的ThreadLocalMap。我不确定它们的性能——但我无法想象它比早期的实现要差得多。

当然new Object()这几天也很快,垃圾回收器也很擅长回收短寿命的对象。

除非你确定创建对象会很昂贵,或者你需要在一个线程一个线程的基础上保持一些状态,你最好在需要时使用更简单的分配解决方案,并且只切换到@ 987654329@ 在分析器告诉您需要时执行。

【讨论】:

+1 是真正解决问题的唯一答案。 你能给我一个不使用 ThreadLocalMap 线性探测的现代 JVM 的例子吗? Java 8 OpenJDK 似乎仍在使用带有线性探测的 ThreadLocalMap。 grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… @Karthick 对不起,我不能。这是我在 2009 年写的。我会更新的。【参考方案4】:

运行未发布的基准测试,ThreadLocal.get 在我的机器上每次迭代大约需要 35 个周期。没什么大不了的。在 Sun 的实现中,Thread 中的自定义线性探测哈希映射将 ThreadLocals 映射到值。因为它只能由单个线程访问,所以速度非常快。

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

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

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

除非您的应用程序大量使用MessageDigest,否则您可能需要考虑使用传统的线程安全缓存。

【讨论】:

恕我直言,最快的方法就是忽略 SPI 并使用 new org.bouncycastle.crypto.digests.SHA1Digest() 之类的东西。我很确定没有缓存可以打败它。【参考方案5】:

好问题,我最近一直在问自己。为了给你确定的数字,下面的基准测试(在 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)

可用here,在 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)

测试名称:线程本地

线程数: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)

测试名称:线程本地

线程数: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 实现和这些架构上,它似乎也可以很好地扩展处理器的数量。

【讨论】:

+1 感谢成为唯一提供定量结果的人。我有点怀疑,因为这些测试是在 Scala 中进行的,但就像你说的,Java 字节码应该是相似的...... 谢谢!这个 while 循环产生的字节码实际上与相应的 Java 代码产生的字节码相同。不过,在不同的虚拟机上可以观察到不同的时间 - 这已经在 Sun JVM1.6 上进行了测试。 此基准代码没有模拟 ThreadLocal 的良好用例。在第一种方法中:每个线程在内存中都会有一个共享的表示,字符串不会改变。在第二种方法中,您可以对哈希表查找的成本进行基准测试,其中字符串在所有线程之间是分离的。 字符串没有改变,但它是在第一种方法中从内存中读取的("!" 的写入永远不会发生) - 第一种方法实际上等效于子类化 Thread 并给它一个自定义场地。该基准测试了一个极端情况,其中整个计算包括读取本地变量/线程 - 实际应用程序可能不会受到影响,具体取决于它们的访问模式,但在最坏的情况下,它们的行为将与上述相同。【参考方案6】:

这是另一个测试。结果显示 ThreadLocal 比常规字段慢一点,但顺序相同。大约慢 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-结束线程本地示例:6015

1-跑场示例

1-End 字段示例:5095

1-运行线程本地示例

1-End 线程本地示例:5720

2-跑场示例

2-End 字段示例:4842

2-运行线程本地示例

2-End 线程本地示例:5835

3-跑场示例

3-End 字段示例:4674

3-运行线程本地示例

3-End 线程本地示例:5287

4-跑场示例

4-End 字段示例:4849

4-运行线程本地示例

4-End 线程本地示例:5309

5-跑场示例

5-End 字段示例:4781

5-运行线程本地示例

5-End 线程本地示例:5330

6-跑场示例

6-End 字段示例:5294

6-运行线程本地示例

6-End 线程本地示例:5511

7-跑场示例

7-End 字段示例:5119

7-运行线程本地示例

7-End 线程本地示例:5793

8-跑场示例

8-End 字段示例:4977

8-运行线程本地示例

8-End 线程本地示例:6374

9-跑场样例

9-End 字段示例:4841

9-运行线程本地示例

9-End 线程本地示例:5471

字段平均值:5051

ThreadLocal 平均:5664

环境:

openjdk 版本“1.8.0_131”

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

Ubuntu 16.04 LTS

【讨论】:

抱歉,这还不是一个有效的测试。 A)最大的问题:您在每次迭代时都分配字符串(Int.toString),与您正在测试的相比,这非常昂贵。B)您每次迭代都执行两个映射操作,也完全不相关且昂贵。尝试从 ThreadLocal 增加一个原始 int 。 C) 使用System.nanoTime 而不是System.currentTimeMillis,前者用于分析,后者用于用户 日期时间目的,并且可以在您的脚下更改。 D)您应该完全避免分配,包括“示例”类的***分配

以上是关于ThreadLocal 变量的性能的主要内容,如果未能解决你的问题,请参考以下文章

知道ThreadLocal吗?一起聊聊到底有啥用

超强解析:ThreadLocal的使用与原理,关键点都在里面

ThreadLocal 原理解析(并不能解决多线程共享数据安全问题)

ThreadLocal 变量的性能

ThreadLocal 变量的性能

WebApp 中的静态 ThreadLocal 变量 - 是不是存在任何安全/性能问题?