91

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

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

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

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

4

6 回答 6

58

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

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

除非您确定创建对象会很昂贵,或者您需要逐个线程地保持某些状态,否则您最好在需要时使用更简单的分配解决方案,并且仅在ThreadLocal探查器时切换到实现告诉你你需要。

于 2009-03-04T11:36:20.250 回答
41

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

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

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

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

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

于 2009-03-04T11:56:55.117 回答
36

好问题,我最近一直在问自己。为了给你确定的数字,下面的基准测试(在 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 实现和这些架构上,它似乎也可以很好地扩展处理器的数量。

于 2011-01-21T07:59:41.400 回答
3

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

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

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

于 2009-03-04T10:14:47.717 回答
3

这是另一个测试。结果显示 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-结束字段样本:6044

0-运行线程本地示例

0-结束线程局部样本:6015

1-跑场样本

1-结束字段样本:5095

1-运行线程本地示例

1-结束线程本地样本:5720

2-跑场样本

2 端字段样本:4842

2-运行线程本地示例

2端螺纹局部样品:5835

3-跑场样本

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”

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

Ubuntu 16.04 LTS

于 2017-09-07T09:19:50.237 回答
0

建造它并测量它。

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

于 2009-03-04T09:42:48.090 回答