【大白话系列】-- ThreadLocal

1、ThreadLocal是什么?

首先,他本质是一个数据结构,有点像HashMap,可以保存 key:value 键值对,但是不同的是,ThreadLocal只能保存一对键值对。

ThreadLocal基本用法:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("小王");
String name = localName.get();

既然他叫ThreadLocal,那肯定跟线程有关,他最重要的特性就是 各个线程的数据互不干扰 !比如上面代码中:线程 1 中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值“小王”,同时在线程1中通过localName.get()可以拿到之前设置的值。但是如果在线程2中,拿到的将是一个null。所以很多叫ThreadLocal为线程本地变量。

 

2、ThreadLocal源码解析

ThreadLocal说白了就是一种数据结构,那么他是怎么做到保证各个线程的数据互不干扰

ThreadLocal类提供如下几个核心方法:

  • get()方法用于获取当前线程的副本变量值。

  • set()方法用于保存当前线程的副本变量值。

  • initialValue()为当前线程初始副本变量值。

  • remove()方法移除当前线程的副本变量值。

重点看set(T value)get()方法的源码:

public void set(T value) {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);//调用getMap方法,取得当前线程的ThreadLocalMap
    if (map != null)	// 往当前线程的ThreadLocalMap中set值
        map.set(this, value);
    else
        createMap(t, value);
}

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

ThreadLocalMap getMap(Thread t) {//返回当前线程的ThreadLocalMap
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

上面代码逻辑很简单,每次set的时候,先获取当前线程,然后从当前线程中获取ThreadLocalMap的一个数据结构(每一个线程都有),然后值保存在当前线程的ThreadLocalMap中。

哪个线程进来,我就set 和get 进来的那个线程本身的ThreadLocalMap,所以各个线程可以互不影响。

3、ThreadLoalMap

本文分析的1.8的源码

从名字上看,可以猜到它是一个类似map的结构,但是它没有实现Map接口。他是初始化一个大小为16的Entry数组,Entry是用来保存key-value键值对的数据结构,只不过这里的key在构造的时候就被限定死了,只能是ThreadLocal对象。这里很有意思,饶了一圈又饶了回去。通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

                                              

值得注意的是:ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,那么怎么解决冲突?

 

4、Hash冲突

ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

 

5、内存泄漏

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

如何避免内存泄露?

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("小王");
    // 其它业务逻辑
} finally {
    localName.remove();//很重要!!!
}

6、ThreadLocal的应用。

6.1 管理Connection

当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

6.2 避免一些参数的传递

6.3 context的传递

每个用户请求页面时,我们都会创建一个任务,类似:

public void process(User user) {
    checkPermission();
    doWork();
    saveStatus();
    sendResponse();
}

然后,通过线程池去执行这些任务。观察process()方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?process()方法需要传递的状态就是User实例。有的童鞋会想,简单地传入User就可以了:

public void process(User user) {
    checkPermission(user);
    doWork(user);
    saveStatus(user);
    sendResponse(user);
}

但是往往一个方法又会调用其他很多方法,这样会导致User传递到所有地方:

void doWork(User user) {
    queryStatus(user);
    checkStatus();
    setNewStatus(user);
    log();
}

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。

典型用法如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();//ThreadLocal实例通常总是以静态字段初始化
void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

//通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:
void step1() {
    User u = threadLocalUser.get();//普通的方法调用一定是同一个线程执行的,所以,step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。
    log();
    printUser();
}

void log() {
    User u = threadLocalUser.get();
    println(u.name);
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值