为什么是0x61c88647?
摘要:在Java1.4之前,ThreadLocals会导致线程争用,使它们对高性能代码毫无用处。
在新的设计中,每个线程都包含自己的线程本地映射,从而提高了吞吐量。然而,由于长时间运行的线程没有从线程本地映射中清除值,我们仍然面临内存泄漏的可能性。
在Java 1.4中,引入了一种新的设计,线程本地变量直接存储在线程中。现在,当我们在线程本地调用get()时,会回调线程,这将返回线程本地映射(ThreadLocalMap)的一个实例,这是线程本地的一个内部类。
我通过实验发现,当一个线程退出时,它也会移除所有的线程本地值。这发生在它被垃圾收集之前,在其exit()方法中。如果我们因此忘记在线程本地上调用remove(),那么当线程退出时,该值仍然可以进行垃圾收集。
线程本地映射包含对线程本地的WeakReferences和对值的普通强引用。然而,它不会查询引用队列来发现哪些弱引用已经被清除,因此条目可能不会立即从线程本地映射中清除(或者根本不会,正如我们将看到的。(
在我们深入研究代码并试图弄清楚线程本地映射是如何工作的之前,我想演示一个如何使用线程本地映射的简单例子。假设我们有一个StupidInhouseFramework,作者从构造函数中调用了一个抽象方法。大概是这样的:
public abstract class StupidInhouseFramework {
private final String title;
protected StupidInhouseFramework(String title) {
this.title = title;
draw();
}
public abstract void draw();
public String toString() {
return "StupidInhouseFramework " + title;
}
}
你可能认为没有人会从构造函数中调用抽象方法,但你错了。我甚至在JDK找到了这样做的地方,尽管我不记得它们在哪里。下面是这个可怜的用户构建的类:
public class PoorUser extends StupidInhouseFramework {
private final Long density;
public PoorUser(String title, long density) {
super(title);
this.density = density;
}
public void draw() {
long density_fudge_value = density + 30 * 113;
System.out.println("draw ... " + density_fudge_value);
}
public static void main(String[] args) {
StupidInhouseFramework sif = new PoorUser("Poor Me", 33244L);
sif.draw();
}
}
当我们运行这个时,我们得到一个NullPointerException。该字段的类型为包装类Long。从超类调用draw()方法,此时还没有调用PoorUser的构造函数。因此,它仍然被设置为空,这将在取消装箱时导致NullPointerException。我们可以使用ThreadLocal解决这个问题,尽管这不是典型的用例,但看起来很有趣。
public class HappyUser extends StupidInhouseFramework {
private final Long density;
private static final ThreadLocal<Long> density_param =
new ThreadLocal<Long>();
private static String setParams(String title, long density) {
density_param.set(density);
return title;
}
private long getDensity() {
Long param = density_param.get();
if (param != null) {
return param;
}
return density;
}
public HappyUser(String title, long density) {
super(setParams(title, density));
this.density = density;
density_param.remove();
}
public void draw() {
long density_fudge_value = getDensity() + 30 * 113;
System.out.println("draw ... " + density_fudge_value);
}
public static void main(String[] args) {
StupidInhouseFramework sif = new HappyUser("Poor Me", 33244L);
sif.draw();
}
}
只是一个警告,JDK 6中线程本地的JavaDocs内部的例子有一个明显的错别字,但幸运的是他们在JDK 7中修复了它。看看能不能看出区别。 线程本地值何时可以被垃圾收集? 我们已经说过,当拥有线程退出时,它们可以被垃圾收集。然而,如果线程属于一个线程池,就像我们在一些应用程序服务器中发现的那样,这些值可能被垃圾收集,也可能不被垃圾收集。 为了演示这一点,我用finalize()方法创建了几个类,它们演示了对象何时到达其生命的终点。 第一个是一个简单的值,它也显示何时收集它并通知我们的测试框架。这将允许我们编写实际的单元测试来证明我们的发现。
public class MyValue {
private final int value;
public MyValue(int value) {
this.value = value;
}
protected void finalize() throws Throwable {
System.out.println("MyValue.finalize " + value);
ThreadLocalTest.setMyValueFinalized();
super.finalize();
}
}
MyThreadLocal覆盖ThreadLocal,并在完成时打印出一条消息:
public class MyThreadLocal<T> extends ThreadLocal<T> {
protected void finalize() throws Throwable {
System.out.println("MyThreadLocal.finalize");
ThreadLocalTest.setMyThreadLocalFinalized();
super.finalize();
}
}
线程本地用户是一个封装线程本地的类。当它不再可达时,我们期望它的ThreadLocal也被收集。请注意,在JavaDocs中,我们看到:ThreadLocal实例通常是私有静态类中希望将状态与线程相关联的字段(例如,用户标识或事务标识)。通过构建许多线程本地的实例,我们以一种更戏剧化的方式展示了这个问题。
public class ThreadLocalUser {
private final int num;
private MyThreadLocal<MyValue> value =
new MyThreadLocal<MyValue>();
public ThreadLocalUser() {
this(0);
}
public ThreadLocalUser(int num) {
this.num = num;
}
protected void finalize() throws Throwable {
System.out.println("ThreadLocalUser.finalize " + num);
ThreadLocalTest.setThreadLocalUserFinalized();
super.finalize();
}
public void setThreadLocal(MyValue myValue) {
value.set(myValue);
}
public void clear() {
value.remove();
}
}
最后一个类是MyThread,它显示线程何时被收集:
public class MyThread extends Thread {
public MyThread(Runnable target) {
super(target);
}
protected void finalize() throws Throwable {
System.out.println("MyThread.finalize");
ThreadLocalTest.setMyThreadFinalized();
super.finalize();
}
}
前两个测试案例说明了当使用remove()方法清除线程本地时,以及当让垃圾收集器处理线程本地时,会发生什么情况。布尔用来帮助我们编写单元测试。
import junit.framework.TestCase;
import java.util.concurrent.*;
public class ThreadLocalTest extends TestCase {
private static boolean myValueFinalized;
private static boolean threadLocalUserFinalized;
private static boolean myThreadLocalFinalized;
private static boolean myThreadFinalized;
public void setUp() {
myValueFinalized = false;
threadLocalUserFinalized = false;
myThreadLocalFinalized = false;
myThreadFinalized = false;
}
public static void setMyValueFinalized() {
myValueFinalized = true;
}
public static void setThreadLocalUserFinalized() {
threadLocalUserFinalized = true;
}
public static void setMyThreadLocalFinalized() {
myThreadLocalFinalized = true;
}
public static void setMyThreadFinalized() {
myThreadFinalized = true;
}
private void collectGarbage() {
for (int i = 0; i < 10; i++) {
System.gc();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void test1() {
ThreadLocalUser user = new ThreadLocalUser();
MyValue value = new MyValue(1);
user.setThreadLocal(value);
user.clear();
value = null;
collectGarbage();
assertTrue(myValueFinalized);
assertFalse(threadLocalUserFinalized);
assertFalse(myThreadLocalFinalized);
}
// weird case
public void test2() {
ThreadLocalUser user = new ThreadLocalUser();
MyValue value = new MyValue(1);
user.setThreadLocal(value);
value = null;
user = null;
collectGarbage();
assertFalse(myValueFinalized);
assertTrue(threadLocalUserFinalized);
assertTrue(myThreadLocalFinalized);
}
}
在test3()中,我们演示了线程关闭是如何释放其线程本地值的:
public void test3() throws InterruptedException {
Thread t = new MyThread(new Runnable() {
public void run() {
ThreadLocalUser user = new ThreadLocalUser();
MyValue value = new MyValue(1);
user.setThreadLocal(value);
}
});
t.start();
t.join();
collectGarbage();
assertTrue(myValueFinalized);
assertTrue(threadLocalUserFinalized);
assertTrue(myThreadLocalFinalized);
assertFalse(myThreadFinalized);
}
在我们的下一个测试中,我们将看到线程池是如何导致值被限制的:
public void test4() throws InterruptedException {
Executor singlePool = Executors.newSingleThreadExecutor();
singlePool.execute(new Runnable() {
public void run() {
ThreadLocalUser user = new ThreadLocalUser();
MyValue value = new MyValue(1);
user.setThreadLocal(value);
}
});
Thread.sleep(100);
collectGarbage();
assertFalse(myValueFinalized);
assertTrue(threadLocalUserFinalized);
assertTrue(myThreadLocalFinalized);
}
到目前为止,我们还没有看到任何重大的惊喜。现在我们来看有趣的测试用例。在下一个例子中,我们构建了一百个线程本地,然后在最后对它们进行垃圾收集。请注意,没有一个MyValue对象是垃圾收集的:
public void test5() throws Exception {
for (int i = 0; i < 100; i++) {
ThreadLocalUser user = new ThreadLocalUser(i);
MyValue value = new MyValue(i);
user.setThreadLocal(value);
value = null;
user = null;
}
collectGarbage();
assertFalse(myValueFinalized);
assertTrue(threadLocalUserFinalized);
assertTrue(myThreadLocalFinalized);
}
在test6()中,我们看到由于强制垃圾收集,一些值现在正在被收集,但是它们落后于ThreadLocalUser收集。
public void test6() throws Exception {
for (int i = 0; i < 100; i++) {
ThreadLocalUser user = new ThreadLocalUser(i);
MyValue value = new MyValue(i);
user.setThreadLocal(value);
value = null;
user = null;
collectGarbage();
}
assertTrue(myValueFinalized);
assertTrue(threadLocalUserFinalized);
assertTrue(myThreadLocalFinalized);
}
您可以看到我的值集合在输出中是如何落后的。到程序结束时,我的值98和99还没有被收集。
ThreadLocalUser.finalize 96
MyValue.finalize 94
ThreadLocalUser.finalize 97
MyThreadLocal.finalize
MyValue.finalize 96
MyValue.finalize 95
MyThreadLocal.finalize
ThreadLocalUser.finalize 98
ThreadLocalUser.finalize 99
MyThreadLocal.finalize
MyValue.finalize 97
凝视ThreadLocal
当我在ThreadLocal类中查看时,我首先注意到的一件事是一个巨大的胖数字0x61c88647正盯着我。这是HASH _ INDENCE。每次创建新的线程本地时,它都会通过将0x61c88647添加到先前的值中来获得一个唯一的哈希号。昨天大部分时间我都在想为什么工程师们选择了这个特定的数字。如果你搜索61c88647,你会发现一些中文文章和一些与加密有关的文章。除此之外,没什么别的了。
我的朋友约翰·格林想把这个数字变成十进制,然后重复搜索。数字1640531527有更多有用的点击。然而,在我们所看到的上下文中,它被用来在散列中相乘散列值,而不是相加它们。此外,在我们找到的所有上下文中,实际数字是-1640531527。进一步挖掘发现,这个数字是无符号数字2654435769的32位有符号版本。
这个数字代表黄金比例(sqrt(5)-1)乘以2的31次方。结果就是一个金色的数字,要么是2654435769,要么是-1640531527。您可以在这里看到计算结果:
public class ThreadHashTest {
public static void main(String[] args) {
long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
System.out.println("as 32 bit unsigned: " + l1);
int i1 = (int) l1;
System.out.println("as 32 bit signed: " + i1);
System.out.println("MAGIC = " + 0x61c88647);
}
}
更多关于黄金比例的信息,请看维基百科链接以及一个关于C++数据结构的书。为了完整起见,我还查阅了唐纳德·克努特(Donald Knuth)的《计算机编程艺术》(The Art of Computer Programming)中对黄金比例的引用。唐纳德·克努特属于每一个严肃的计算机程序员,就像默克手册装饰你的健康从业者的书架一样(在线提供)。只是不要指望理解内容...
因此,我们利用黄金比例,确定HASH _ INDENTATION与斐波那契散列法有关。如果我们仔细观察在ThreadLocalMap中散列的方式,我们就会明白为什么这是必要的。标准的java.util.HashMap使用链表来解决冲突。ThreadLocalMap只是寻找下一个可用空间,并在那里插入元素。它通过位屏蔽找到第一个空间,因此只有低几个位是有效的。如果第一个空间已满,它只需将元素放在下一个可用空间中。HASH _ INDENTATION在sparce哈希表中将键隔开,这样就减少了在我们的哈希表旁边找到一个值的可能性。
当线程本地被垃圾收集时,线程本地映射中的WeakReference键被清除。我们接下来需要解决的问题是何时从ThreadLocalMap中删除它。确实如此不当我们打电话的时候就可以离开了get()地图上的另一个条目。java.util.WeakHashMap删除地图上所有过时的条目get()。get()在ThreadLocalMap中会快一点,但可能会留下过时的条目,从而导致内存泄漏。
当线程本地设置为()时,它可以分为三类:
首先,我们可以找到条目并简单地设置它。在这种情况下,过时的条目根本不会被删除。
其次,我们可能会发现我们之前的一个条目已经过时,在这种情况下,我们会在运行中删除所有过时的条目(也就是说,在两个之间空值)。如果我们找到了我们的钥匙,它将与旧的条目交换。
第三,我们的运行可能没有足够的空间来扩展,在这种情况下,条目被放在运行的最后一个空值中,一些过时的条目被清除。这个阶段最初使用0(log2n)算法,但是如果它不能低于填充因子,则在0(n)中执行完全重新散列。
最后,如果删除了一个关键字,那么该条目将与该运行中的任何其他条目一起被删除。
最佳实践
如果必须使用ThreadLocal,请确保在完成后立即移除该值,最好是在将线程返回线程池之前。最佳实践是remove()而不是设置set(null),因为这将导致WeakReference立即与值一起被移除。