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:它的行为当然是这样,但通常不会以这种方式实现。相反,Thread
s 包含一个(未同步的)哈希图,其中键是当前的 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
中的自定义线性探测哈希映射将 ThreadLocal
s 映射到值。因为它只能由单个线程访问,所以速度非常快。
小对象的分配需要类似数量的周期,但由于缓存耗尽,您可能会在紧密循环中获得较低的数字。
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的使用与原理,关键点都在里面