ThreadLocal之实现原理

好久木有来更新了,下面来记录并分享下ThreadLocal的实现原理:

下面让我们一起深入ThreadLocal的内部实现。
我们需要关注的,自然是ThreadLocal的set()方法和get()方法。从set()方法先说起:

public void set(T value){
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if(map != null)
		map.set(this,value);
	else
		createMap(t, value);
}

在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是你可以把它简单地理解成HashMap),但是它是定义在Thread内部的成员。注意下面的定义是从Thread类中摘出来的:

ThreadLocal.ThreadLocalMap.threadLocals = null;

而设置到ThreadLocal中的数据,也正是写入了threadLocals这个Map。其中key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。
在进行get()操作时,自然就是将这个Map中的数据拿出来:

public T get(){
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if(map != null){
		ThreadLocalMap.Entry e = map.getEntry(this);
		if(e != null)
			return (T)e.value;
	}
	return setInitialValue();
}

首先,get()方法也是先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。
在了解了ThreadLocal的内部实现后,我们自然会引出一个问题。那就是这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。
当线程退出时,Thead类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码的加粗部分:

//在线程退出前,由系统回调,进行资源清理
private void exit(){
	if(group != null){
		group.threadTerminated(this);
		group = null;
	}
	target = null;
	//加速资源清理
	threadLocals = null;
	inheritableThreadLocals = null;
	inheritedAccessControlContext = null;
	blocker = null;
	uncaughtExceptionHandler = null;
}

因此,如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在的)。如果这样,将一些大大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄露的可能(设置了对象到ThreadLocal中,但是不清理它,在使用几次后,这个对象也就不再有用了,但是它却无法被回收)。

此时,如果希望及时收回对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。
另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null之类的代码。如果这么做,obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。
同理,如果对于ThreadLocal的变量,我们也动手将其设置为null,比如t1=null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。这里面的奥秘是什么呢?先来看一个简单的例子:

public class ThreadLocalDemo_Gc{
	static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>(){
		protected void finalize() throws Throwable{
			System.out.println(this.toString() + "is gc");
		}
	};
	static volatile CountDownLatch cd = new CountDownLatch(10000);
	public static class ParseDate implements Runnable{
		int i=0;
		public ParseDate(int i){
			this.i = i;
		}
		public void run(){
			try{
				if(tl.get() == null){
					t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")){
						protected void finalize() throws Throwable{
							System.out.println(this.toString() + "is gc");
						}
					});
					System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
				}
				Date t = tl.get().parse("2019-04-23 10:47:"+i%60);
			}catch(ParseException e){
				e.printStackTrace();
			}finally{
				cd.countDown();
			}
		}
	}
	public static void main(String[] args) throws InterruptedException{
		ExecutorService es = Executors.newFixedThreadPool(10);
		for(int i=0; i<10000; i++) {
			es.execute(new ParseDate(i));
		}
		cd.await();
		System.out.println("mission complete!!");
		tl = null;
		System.gc();
		System.out.println("first GC complete");
		//在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
		tl = new ThreadLocal<SimpleDateFormat>();
		cd = new CountDownLatch(1000);
		for(int i=0; i<1000; i++) {
			es.execute(new ParseDate(i));
		}
		cd.await();
		Thread.sleep(1000);
		System.gc();
		System.out.println("second GC complete");
	}
}

上述案例是为了跟踪ThreadLocal对象以及内部SimpleDateFormat对象的垃圾回收。为此,我们在第3行和第17行,重载了finalize()方法。这样,我们在对象被回收时,就可以看到它们的踪迹。
在主函数main中,先后进行了两次任务提交,每次1000个任务。在第一次任务提交后,代码第39行,我们将tl设置为null,接着再进行一次GC。接着,我们进行第2次任务提交,完成后,在第50行再进行一次GC。
注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到hreadLocal对象被回收了。接着提交了第2次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第二次GC。可以看到,在第2次GC后,第一次创建的10个SimpleDateFormat子类全部被回收。可以看到,虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们。
要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西,更加精确地说,它更加类似于WeakHashMap。
ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference的ThreadLocal集合。

static class Entry extends WeakReference<ThreadLocal>{
	Object value;
	Entry(ThreadLocal k , Object v) {
		super(k);
		value = v;
	}
}

这里的参数k就是Map的key,v就是Map的value。其中k也就是ThreadLocal实例,作为弱引用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Map的key,但实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描),它会自然而然将这些垃圾数据回收。
三、对性能有何帮助
每一个线程分配一个独立的对象对系统性能也是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。
这里,我们简单的测试下在多线程下产生随机数的性能问题。首先,我们定义一些全局变量:

public static final int GEN_COUNT = 10000000;
public static fianl int THREAD_COUNT = 4;
static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
public static Random rnd = new Random(123);
public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){
	@Override
	protected Random initialValue(){
		return new Random(123);
	}
};

代码第1行定义了每个线程要产生的随机数数量,第2行定义了参与工作的线程数量,第3行定义了线程池,第4行定义了被多线程共享的Random实例用于产生随机数,第6-11行定义了由ThreadLocal封装的Random。
接着,定义一个工作线程的内部逻辑。它可以工作在两种模式下:
第一是多线程共享一个Random(mode =0)。(耗时长)
第二是多个线程各分配一个Random(mode=1)。(耗时短)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值