ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

记录一下学习内容,若有错误感谢指正,互相交流,共同进步

ThreadLocal

摘要:

在堆内存中,有一个线程共享的哈希表ThreadLocalMap,可以通过该表实现线程内的参数传递,如用户信息、应用信息等基础信息,以便在方法调用过程中不需要重复传递,ThreaLocal更像是一个操作该表的工具对象

关系图

ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享 

//执行应用代码
ThreadLocal<String> userId=new ThreadLocal<String>()
userId.set("1234")
//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 源码
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

当我们创建一个ThreadLocal对象 userId,并赋值1234时,通过源码发现,值内容“1234”并非保存在ThreadLocal对象内,而是通过ThreadLocal获取当前线程对象,并通过该对象获取当前线程所对应的ThreadLocalMap 对象,值内容“1234”保存在ThreadLocalMap 内的Entry对象的value里,而key则是ThreadLocal对象实例,此处注意Key为弱引用,后续会介绍

ThreadLocal 应用

场景之一,共享基础信息

1、创建工具类UserRuntimeEnv 定义基础信息对象

public class UserRuntimeEnv {
    //这里的USERID和PHONE是 ThreadLocal对象的key
    private static final ThreadLocal<String> USERID=new ThreadLocal<>();
    private static final ThreadLocal<String> PHONE =new ThreadLocal<>();

    public UserRuntimeEnv() {
    }
    public static String getPhone(){
        return (String)PHONE.get();
    }
    public static void setPhone(String phone){
        PHONE.set(phone);
    }

    public static void setUserId(String userId) {
        USERID.set(userId);
    }

    public static String getUserId(){
        return USERID.get();
    }

    public static void clear(){
        PHONE.remove();
        USERID.remove();
    }


}

 2、创建Aop类拦截请求,存入基础信息(可忽略自定义注解的内容)

 public Object aroundApi(ProceedingJoinPoint point)throws Throwable{
        String args=argsToString(point.getArgs());
        log.info("日志统一打印 ↓ ↓ ↓ ↓ ↓ ↓ {}.{}() start ↓ ↓ ↓ ↓ ↓ ↓ ↓ ,参数:\n{}",
                point.getSignature().getDeclaringTypeName(),//类名
                point.getSignature().getName(),//方法名
                args);//请求参数
        StopWatch stopWatch=new StopWatch();//spring通用时间记录工具,ms级别
        stopWatch.start();
        RequestAttributes requestAttributes= RequestContextHolder.getRequestAttributes();
        HttpServletRequest request=(HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        String userId=request.getHeader("userId");
        UserRuntimeEnv.setUserId(userId);//将用户信息存放到线程本地表,方便方法直接调用获取参数
        MethodSignature methodSignature=(MethodSignature) point.getSignature();//获取方法对象
        String declaringTypeName=methodSignature.getDeclaringTypeName();
        String methodName=methodSignature.getName();

        Object response=null;
        //获取该方法的LogPrint注解信息,可能为null
        LogPrint logPrint=methodSignature.getMethod().getAnnotation(LogPrint.class);
        try {
            response=point.proceed();
        }finally {
            stopWatch.stop();
            if(null==logPrint||logPrint.isPrint()){
                log.info("统一日志打印(end): {}.{}() ↑ ↑ ↑ ↑ ↑,响应时间:{}毫秒,响应内容:\n{}",
                        declaringTypeName, methodName, stopWatch.getTotalTimeMillis(), argsToString(response));
            }
        }
        //这种情况下,线程已结束,可清除可不清除
        UserRuntimeEnv.clear();
        return response;
    }

 3、在整个线程的方法执行中均可通过

UserRuntimeEnv.getUserId 获取相应用户信息

弱引用

摘要

       对象当可达性只有弱引用时,当系统触发gc时对象会被回收掉

 //这种情况下,线程已结束,可清除可不清除
        UserRuntimeEnv.clear();
//ThreadLocalMap源码
private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

在上述示例中可看到ThreadLocal对象可清除可不清除,不清除不会造成内存泄露吗?不会,因为ThreadLocalMap有个回收机制,从ThreadLocalMap源码可见当设置value时会检索entry对象并清除key为null的对象

也就是说当key为null时对象会自动回收,

那为什么key怎么才会null呢?

这就提到刚刚的弱引用定义,当对象只被弱引用指向时,会在gc中被自动回收。如果key对象只被弱引用指向,则会在gc后变成null

// ThreadLocalMap 源码
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

从ThreadLocalMap 源码可见,正如上图所示,虚线是弱引用,在线程执行中,key指向ThreadLocal是弱引用指向,threadLocalRef(userId的引用)指向ThreadLocal是强引用指向,当线程执行完毕,线程栈内的引用清空,ThreadLocal就只剩下弱引用指向,则会在gc后回收,相应value也会在后续回收

弱引用介绍

        //创建两个强引用指向的对象
        Object objA=new Object();
        Object objB=new Object();

        //创建两个弱引用weakA、weakB和一个强引用strongA;
        WeakReference<Object> weakA=new WeakReference<>(objA);
        WeakReference<Object>weakB=new WeakReference<>(objB);
        Object strongA=objA;

ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

此时取消objA和objB的引用 

        objA=null;
        objB=null;

 ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

执行gc后

 System.gc();

 ThreadLocal 应用及原理、弱引用介绍、同线程内数据共享

执行完后,Object@0002对象被回收,注意这里gc回收的是堆内存里的对象,不是引用对象

 若有错误感谢指正,互相交流,共同进步

 

上一篇:ThreadLocal 内存泄漏的问题


下一篇:【无标题】