ThreadLocal - 一直搞不懂的知识

352 阅读4分钟

TheadLocal简介

ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题!(说白了就是每个线程独有的变量,自然不存在并发问题)

谁再说ThreadLocal解决了并发问题,呼死他,根本没有半毛钱的关系!!!

ThreadLocal实现原理

想要更好地去理解ThreadLocal,那就得翻翻它是怎么实现的了~~~

声明:本文使用的是JDK 1.8

首先,我们来看一下ThreadLocal的set()方法,因为我们一般使用都是new完对象,就往里边set了

    public void set(T value) {
        // 得到当前线程对象
        Thread t = Thread.currentThread();
        // 这里获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);

        // 如果map存在,则将当前的threadlocal对象作为key,要存储的对象作为value存到map里面去
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

上面有个ThreadLocalMap,我们去看看这是什么?

static class ThreadLocalMap {
    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

通过上面我们可以发现的是ThreadLocalMap是ThreadLocal的一个静态内部类,用Entry类来进行存储,我们的值都是存储到这个Map上的,key是当前ThreadLocal对象!

如果该Map不存在,则初始化一个:

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

如果该Map存在,则从Thread中获取!

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Thread维护了ThreadLocalMap变量

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null

从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用静态内部类来编写的,但对象的引用是在Thread中!

于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map成员变量,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象。

有了上面的基础,我们看get()方法就一点都不难理解了:

    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();
    }

ThreadLocal原理总结

  1. 每个Thread维护着一个ThreadLocalMap的引用;
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储;
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象;
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象;
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

ThreadLocal的使用场景

1. 避免重复创建对象

对于一些工具类(SimpleDateFormat,Random),在一次请求中可能被多次用到(需要多次创建对象),每次用的时候就:

    SimpleDateFormat formator = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 
    formator.format(date);

使用ThreadLocal后:

public class DateUtil {
    private static ThreadLocal<DateFormat> yyyyMMdd_HHmmss = new ThreadLocal() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
   
    public static String format_yyyyMMdd_HHmmss(Date date) {
        if (date == null) {
            return null;
        }
        return yyyyMMdd_HHmmss.get().format(date);
    }
    public static Date parse_yyyyMMdd_HHmmss(String date) {
        if (date == null) {
            return null;
        }
        try {
            return yyyyMMdd_HHmmss.get().parse(date);
        } catch (ParseException e) {
            throw new RuntimeException("时间解析异常:" + date, e);
        }
    }
}

非常的好用~~~ (感觉也不咋实用)

2. 避免大量参数传递

toC的应用开发中,对于用户的请求我们服务端一般都会进行风控检验(请求风控入参会包括用户信息、设备信息等),来判断用户是不是危险用户,从而进行限制请求。而用户的设备信息在请求打到服务端时就已经解析拿到。处理链路如下:

image.png 设备信息我们几乎不care,但是方法handle3中调用风控会用到,而不得不一层层透传,非常难受...

使用ThreadLocal后,就变成了:

image.png 非常的好用~~~

// 实际开发中,我们会定义DeviceInfo注解,自动解析用户信息、设备信息放到ThreadLocal中
public class DeviceInfoHolder {
    private static ThreadLocal<BaseDeviceInfo> DEVICE_HOLDER = new ThreadLocal<>();

    public static void setDeviceInfo(BaseDeviceInfo deviceInfo) {
        DEVICE_HOLDER.set(deviceInfo);
    }
    public static BaseDeviceInfo getDeviceInfo() {
        BaseDeviceInfo baseDeviceInfo = DEVICE_HOLDER.get();
        if (baseDeviceInfo == null) {
            throw new IllegalStateException("lack device info");
        }
        return baseDeviceInfo;
    }
    public static void reset() {
        DEVICE_HOLDER.remove();
    }
}

@Aspect
@Component
public class DeviceInfoAspect {

    @Around("@annotation(com.xxx.xxx.annotation.DeviceInfo)")
    public Object deviceInfo(ProceedingJoinPoint joinPoint) throws Throwable {
        AbstractRequest requestBody = null;
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof AbstractRequest) {
                requestBody = (AbstractRequest) arg;
                break;
            }
        }
        // ... 从http请求中解析出用户信息、设备信息
        BaseDeviceInfo deviceInfo = DeviceInfoAssembler.MAPPER.toDeviceInfo(requestBody);
        DeviceInfoHolder.setDeviceInfo(deviceInfo);
        try {
            return joinPoint.proceed();
        } finally {
            DeviceInfoHolder.reset();
        }
    }
}

ThreadLocal的内存泄漏

我们来看一下ThreadLocal的对象关系引用图:

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

想要避免内存泄露就要手动remove()掉


自己画的TheadLocal对象关系引用图: image.png


感谢大佬们: Java并发-ThreadLocalThreadLocal就是这么简单