摘要 :java1.4之前,ThreadLocals处于其对于高性能代码无用的线程争论。在新的设计中,每一个线程包含自己的ThreaLocalMap,这对于性能的提升是全面的,然而,我们依然面临着由于长时间运行线程的ThreadLocalMap的值没有被清理导致的内存泄露问题。
我注意到在我的java专业硕士课程的示例中,并不是每次ThreadLocal的值都能够被回收。这个会导致应用中重复使用的线程的内存泄漏。
在早期JAVA版本中,经常有关于ThreadLocal在并发应用中是否有用的争论,在java1.4中,ThreadLocal使用了新的设计:每一个线程内部直接存储ThreadLocals,现在我们使用get()调用ThreadLocal时,会返回一个ThreadLocalMap实例的ThreadLocal内部类。
当一个线程退出调用exit()时,它的ThreadLocal的values会在CG之前被清除。当然如果我在使用完ThreadLocal时忘记主动调用remove(),ThreadLocal也会随着线程的退出被GC清除。
ThreadLocalMap包含对于ThreadLocals的弱关联和values的强关联(但是,ThreadLocalMap不会主动遍历队列来查看哪一个ThreadLocals被清理,因此当ThreadLocals被清理时,ThreadLocalMap中的记录不会被立即清除(到目前为止,就我所知是这样的)。
在我们深入研究代码来查看ThreadLocalMap是怎么工作之前,我想演示一个关于ThreadLocal如何使用的例子,想象一下我们有一个在其构造器内调用抽象方法的抽象类:StupidInHouseFramework。
public abstract class StupidInhouseFramework {
private String title;
protected StupidInhouseFramework(String title) {
this.title = title;
draw();
}
public abstract void draw();
public String toString() {
return "StupidInhouseFramework " + title;
}
}
你可能会觉得没有人会在构造方法内调用抽象方法,但你是错误的,在jdk中有很多地方都是这么做的。尽管我不记得是在什么位置。下面是PoorUser:
package com.ThreadLocalDemo;
public class PoorUser extends StupidInhouseFramework {
private final Long density;
public PoorUser(String title, long density){
super(title);
this.density = density;
}
@Override
public void draw() {
long desity_fudge_value = density + 30 * 113;
System.out.println("draw..." + desity_fudge_value);
}
public static void main(String[] args) {
StupidInhouseFramework sif = new PoorUser("Poor me", 33244L);
sif.draw();
}
}
当我们启动这个程序时,我们会得到一个NPE(NullPointException),density是一个包装类型的Long类型对象,在superClass中的draw()被调用时,PoorUser的构造方法还没有被调用,因此density依然是个空值,所以在其拆箱的时候抛出了NPE,我们可以使用ThreadLocal来解决这个问题。
package com.ThreadLocalDemo;
public class HappyUser extends StupidInhouseFramework {
private final Long density;
private static final ThreadLocal density_param =
new ThreadLocal();
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();
}
@Override
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();
}
}
提醒:JDK1.6中ThreadLocal的JavaDocs有一个明显的错误,但是已经在JDK1.7中被修复了,感兴趣的话可以去看一下。
我们之前说过ThreadLocal的values会在拥有他的线程退出时被GC,然而如果线程时存在于线程池中时,就像我们在一些应用中看到的,ThreadLocal的values可能永远不会被GC。
为了演示,我创建了若干了含有finalize()的类,finalize是为了展示当对象结束时的情况。
第一个示例中,value会在被我们的测试框架回收的时候被打印,我们可以写单元测试演示。
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> {
@Override
protected void finalize() throws Throwable {
System.out.println("MyThreadLocal.finalize");
ThreadLocalTest.setMyThreadLocalFinalized();
super.finalize();
}
}
ThreadLocalUser是一个封装ThreadLocal的类,当他的实例不可达的时候,我们希望他的ThreadLocal会被回收。从JavaDocs中我们可以知道ThreadLocal实例是一个peivate static字段为了将他的状态和Thread关联起来(例如:用户ID或者事务ID)。通过构造ThreadLocal的大量实例,我们可以以一个更加有趣的方式来展示问题。
```java
public class ThreadLocalUser {
private final int num;
private MyThreadLocal value =
new MyThreadLocal();
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();
}
}
"se-preview-section-delimiter">
最后一个类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();
}
}
"se-preview-section-delimiter">
前两个case是说明当ThreadLocal被remove()方法清理时发生了什么以及什么时候threadLocal会被GC回收,boolean返回值是为了帮助我们写单元测试。
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);
}
}
"se-preview-section-delimiter">
在test3()中,我们展示当一个线程关闭时是如何释放他的ThreadLocal values的:
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);
}
"se-preview-section-delimiter">
在我们下面的测试中,我们可以看到线程池会导致ThreadLocal的value不会被GC。
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);
}
"se-preview-section-delimiter">
目前为止,我们看到的都是在意料之中。现在我们进行一个有趣的测试:在下一个测试中我们构造100个ThreadLocal然后让GC回收他们,注意:没有一个values会被GC回收。
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);
}
"se-preview-section-delimiter">
在test6()中,由于进行强制GC,一些values会被回收,但是他们会在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);
}
"se-preview-section-delimiter">
你可以从output中看到MyValues会怎么被回收,在程序运行结束时,Myvalues 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的源码是首先注意到的事情是一个非常大的数字 0x61c8847 这是HASH_INCREMENT的值,每次创建新的ThreadLocal时,通过目前的值与0x61c8847相加,我昨天花费了大量的时间来搞清楚工程师们为什么选择这个特别的数字,如果你google搜索这个数字,你只会找到一个和加密相关的文章。
我的朋友John Green想到了先将这个数字转换为10进制在进行搜索,1640531527得到了更有用的结果。然而,在我们得到的结果中,这个数字经常用于在散列中增加哈希值,而不是相加。同时,在我们找到的所有结果中,真实的数字是 -1640531527。更多的发现显示这个数字是32位无符号数,他的无符号数是2654435769。
这个数字代表着2的31次方乘以黄金比率(5的平方根-1)(原文:This number represents the golden ratio (sqrt(5)-1) times two to the power of 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++数据结构]http://www.brpreiss.com/books/opus4/html/page214.html),为了更加了解,我还参考了Donald Knuth的编程的艺术,Donald Knuth在程序员中非常有威望,当然还有Merck Manual点缀你的健康从业者书架([网络版]http://www.merck.com/pubs/)),不要期望读懂。。
我们可以确定HASH_INCREMENT使用黄金比例,与斐波那契散列相关,如果我们仔细观察ThreadLocalMap的hashing方式,我们会明白这为什么是必要的。java.util.HashMap
通过链式法解决冲突,但是ThreadLocalMap只是简单的寻找下一个可用位置来插入元素。他首先通过比特掩码(bit masking)来查到第一个位置,因此只有一些低位的比特是有意义的,如果第一个位置非空,就将元素放到下一个可以使用的位置,HASH_INCRMENT在稀疏哈希表中找到位置,因此找到下一个可用的位置的可能性在减少。
当一个ThreadLocal被GC,在ThreadLocalMap中弱关联(引用)的key会被清理,我们下一个需要解决的问题是什么时候key会在ThreadLocalMap中删除,他不会在我们调用get()
获取Map中另外的entry时被清理,java.util.WeakHashMap
会在get()
时会清理自身过期的enties,所以ThreadLocalMap中的get()可能会快一些,但是可能会含有过期的enties,导致内存泄漏。
当一个ThreadLocal使用set(),可能遇到下面三种情况:
1. 第一种,可以直接设置entry(即直接查找到的位置为空),在这个环节,过期的enties不会被清理。
2. 其次,我们可能发现在我们的entry失效之前另一个entry失效,这种情况下会在我们的程序运行过程中清理所有的失效的enties(也就是说在两个空值之间),如果找到我们失效的entry,会随着失效的entry一个被清理。(ps:这一条没看懂,他跟set()有什么关系啊。。。难道是set(null)?)
3. 第三,我们可能没有足够的空间插入entry,在这种情况下entry会被放置在最后null位置,并且清理那些过期的enties,这一阶段采用O(log2n)的填充因子,如果超过这个值会进行一个O(n)的扩容。
最后,如果一个key被移除,那么会随着程序其他的enties失效。
如果你想知道更多关于这个是如何工作的,你可以从 Knuth 6.4 Algorithm R获取更多信息。
如果你必须使用ThreadLocal,确保你在你在使用完之后和结束你的线程返回线程池之前尽快的清理value,最好的做法是使用remove()
而不是set(null)
,因为他的value的弱关联随之一起立即被清理。
如果你必须使用ThreadLocal,确保你在你在使用完之后和结束你的线程返回线程池之前尽快的清理value,最好的做法是使用remove()
而不是set(null)
,因为他的value的弱关联随之一起立即被清理。
原文链接: