【JAVA核心知识】18:线程本地变量-ThreadLocal

ThreadLocal是JDK1.2引入的类,用来提供线程内的局部变量。不同于普通变量,线程本地变量与线程绑定,每个线程都有自己独立的变量容器。线程本地变量在线程的生命周期内起作用,用来减少一个线程内多个函数或组件之间公共变量的传输复杂度。

ThreadLocal

使用示例

实现剖析


使用示例

public class ThreadLocalDemo {

    static final ThreadLocal<String> TL_DEMO = new ThreadLocal<>();
    public static void main(String[] args) {
        Runnable rn1 = new Runnable() {
            @Override
            public void run() {
                setVal("R1 TEST STRING");
                printVal();
            }
        };
        Runnable rn2 = new Runnable() {
            @Override
            public void run() {
                setVal("R2 TEST STRING");
                printVal();
            }
        };
        new Thread(rn1).start();
        new Thread(rn2).start();
    }
    
    public static void setVal(String val) {
        TL_DEMO.set(val);
    }

    public static void printVal() {
        System.out.println(TL_DEMO.get());
    }
}

运行结果:

R1 TEST STRING
R2 TEST STRING

看到这里,你可能会说,这个我用Map就能实现,为什么要用ThreadLocal呢。首先ThreadLocal与线程绑定,各个线程之间相互独立不互相影响,你可以在任何地方通过ThreadLocal获得与当前线程绑定的线程本地变量。当然这个也是通过以线程标识做Key的Map去实现的。其次最重要的是ThreadLocal的存储的线程本地变量的会随着线程的销毁而自动进入可回收状态,而使用Map则需要在线程消亡之前显示的进行Map的清除操作,否则变量一直存在强连接,无法进入回收状态。

虽然ThreadLocal实现的功能我们通过其它途径都能实现,但是都没有ThreadLocal这么简单,工具类存在的意义就在这里,将复杂的功能实现进行集成并提供简单的使用方式,使编程更简单,代码更清晰。

实现剖析

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

通过上面的set方法可以看到Thread的实际实现是通过Map实现的,Thread内部维护了属性threadLocals:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap的容器为Thread$ThreadLocalMap$Entry[],引用关系如图:

而对于Thread$ThreadLocalMap$Entry :

private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
	Object value;

	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}

可以看到Entry继承了WeakReference,这说明Entry对ThreadLocal的引用是弱引用,意味着当我们持有的ThreadLocal强引用消失后,ThreadLocal实例即可进入可回收状态。但是Entry实例并不会跟着进入回收状态。

对于普通线程因为ThreadLocalMap维护在Thread内部,因此当Thread死亡时,其维护的线程本地变量容器ThreadLocalMap也会进入可回收状态。但是对于线程池线程,我们知道线程池运行过程中线程池的核心线程在未设置的情况下是不会销毁的,这意味着线程维护的ThreadLocalMap在线程池运行期间会一直存在,此时可以通过手动调用ThreadLocal.remove()来使Entry实例的强引用消失进入可回收状态。

但是也并没有一个地方说使用ThreadLocal需要强制调用ThreadLocal.remove()方法呀,这样一直不回收又一直有新对象进来的话为什么不会造成OOM呢?这是因为ThreadLocal会有一个自动的懒清理方式。ThreadLocalMap的存储方式不同于HashMap采用的拉链法,而是采用的开地址法

/**下标确认*/
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)])
/**下标确认*/

private static int nextIndex(int i, int len) {
	return ((i + 1 < len) ? i + 1 : 0);
}

遭遇Hash冲突且发现存在Key已经被回收的无效Entry实例时,则会尝试性的进行一部分容器槽的扫描清除操作(不仅仅局限于冲突的那个,而是一定范围的扫描),使得Entry进入可回收状态。

那么为什么不将value也设置成弱引用呢?这样不是就可以不用手动执行ThreadLocal.remove(),达到像ThreadLocal实例一样的自动回收呢?这是因为ThreadLocal无法确定程序逻辑会在什么时间断开对value实例的强引用持有,如果将value也设置成弱引用,那么在程序逻辑失去强引用持有后,value就会在下一次GC时被回收,但是也许此时程序还持有ThreadLocal实例,并在接下来的运行中通过ThreadLocal实例去获取线程本地变量value,然后此时value却已被GC,造成意料之外的错误。

尽管如此,线程池模式下使用ThreadLocal依然建议在合适的地方调用ThreadLocal.remove()进行对象的及时回收,避免造成内存泄漏。

PS:
【JAVA核心知识】系列导航 [持续更新中…]
上篇导航:17.2:线程间通信协作-Exchanger
下篇预告:19:JAVA中的各种锁
欢迎关注…

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yue_hu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值