ThreadLocal总结

并发编程
1、同一线程,不同组件中传递数据。
2、线程隔离,每一个线程都是独立的,互不影响。

结构及用法
1、ThreadLocal 的内部 ThreadLocalMap,键为 ThreadLocal。
2、ThreadLocal 的数据结构是个环形数组
3、get,set 两个方法都不能完全防止内存泄漏,还是每次用完 ThreadLocal 都勤奋的 remove一下靠谱。
4、ThreadLocalMap 采用开放地址法,ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。

ThreadLocalMap 的 Entry 的 key 是弱引用,如果外部没有强引用指向 key,key 就会被回收,而 value 由于 Entry 强引用指向了它,导致无法被回收,但是 value 又无法被访问,因此发生内存泄漏。
关于内存泄漏,我们重点从源码层面分析了 get、set、remove 方法,并图文并茂的演示了 get、set 方法不能防止内存泄漏,而 remove 方法能防止内存泄漏的结论。

问题1
1、ThreadLocal中有有使用弱引用,为什么要用弱引用?用弱引用,发生一次gc后,set进去的值再get就是null了吗?

图可以看出,ThreadLocal作为key,是有两条引用链的,一条是当前线程中的,由线程指向ThreadLocalMap,通过Map指向Entry,而Entry指向key;另一条引用链则是当前执行的主线程类的成员变量,且为强引用,所以目前来说并不会受到gc影响。

public class TestThreadLocalLeak {
    final static ThreadLocal<byte[]> LOCAL = new ThreadLocal();
    final static int _1M = 1024 * 1024;

    public static void main(String[] args) {
        //testUseThread();
        testUseThreadPool();
    }

    /**
     * 使用线程
     */
    private static void testUseThread() {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    LOCAL.set(new byte[_1M])
            ).start();
        }
    }

    /**
     * 使用线程池
     */
    private static void testUseThreadPool() {
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 100; i++) {
            executorService.execute(() ->
                    LOCAL.set(new byte[_1M])
            );
        }
        executorService.shutdown();
    }
}

当调用testUseThread()时,系统在运行时执行了大量YGC,但始终稳定回收,最后正常执行,但是执行testUseThreadPool()时,经历的频繁的Full GC,内存却没有降下去,最终发生了OOM。
我们分析一下,在使用new Thread()的时候,当线程执行完毕时,随着线程的终止,那个这个Thread对象的生命周期也就结束了,此时该线程下的成员变量,ThreadLocalMap是GC Root不可达的,同理,下面的Entry、里面的key、value都会在下一次gc时被回收;而使用线程池后,由于线程执行完一个任务后,不会被回收,而是被放回线程池以便执行后续任务,自然其成员变量ThreadLocalMap不会被回收,最终引起内存泄露直至OOM。至于怎么避免出现内存泄露,就是在使用线程完成任务后,如果保存在ThreadLocalMap中的数据不必留给之后的任务重复使用,就要及时调用ThreadLocal的remove(),这个方法会把ThreadLocalMap中的相关key和value分别置为null,就能在下次GC时回收了。

问题2
为什么用ThreadLocal做key?ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
} 


同一个Thread中有一个ThreadLocalMap,ThreadLocalMap中包含一个Entry[] 数组。可以存放多个ThreadLocal,KEY VALUE。

问题3
Entry的key为什么设计成弱引用?
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。
就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的get、set或remove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。
此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
这样就能最大程度的解决内存泄露问题
如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。

下面看看弱引用的例子:

WeakReference<String> weakReference = new WeakReference<>(new String("123"));
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
结果:
123
null

传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。
如果这种情况:

String s = "123";
WeakReference<String> weakReference = new WeakReference<>(s);
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
结果:
123
123

先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。
我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。
如果将object强引用设置为null,第二次gc之后,弱引用能够被正常回收。
此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?
答:Entry的value不只是被Entry引用,有可能被业务系统中的很多地方引用了。如果value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。
而相比之下,Entry的key,引用的地方就非常明确了。
这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。

问题4
ThreadLocal真的会导致内存泄露?
假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。
那么,Entry的value值一直都没被清空。
所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。
其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。

问题5
如何解决内存泄露问题?
调用ThreadLocal对象的remove方法,及时删除value。
remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

问题6
ThreadLocal是如何定位数据的?
在ThreadLocal的get、set、remove方法中都有这样一行代码:

int i = key.threadLocalHashCode & (table.length - 1);

通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
这是一种hash算法。
接下来给大家举个例子:假设len=16,key.threadLocalHashCode=31,
于是:int i = 31 & 15 = 1
相当于:int i = 31 % 15 = 1
计算的结果是一样的,但是使用与运算效率跟高一些。

问题7
扩容的关键步骤如下:

老size + 1 = 新size
如果新size大于等于老size的2/3时,需要考虑扩容。
扩容前先尝试回收一次key为null的值,腾出一些空间。
如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
每次都是按2倍的大小扩容。

问题8
父子线程如何共享数据?
使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类

public class ThreadLocalTest {

    public static void main(String[] args) {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
}
结果:
父线程获取数据:6
子线程获取数据:6

问题9
线程池中如何共享数据?
我们应该使用InheritableThreadLocal,具体代码如下:

public class OTest {
    public static void main(String[] args) {
        OTest.fun1();
    }

    private static void fun1() {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        threadLocal.set(6);
        executorService.submit(() -> {
            System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
        });

        threadLocal.set(7);
        executorService.submit(() -> {
            System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
        });
    }
}

结果:
父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:6

由于这个例子中使用了单例线程池,固定线程数是1。

第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。

因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

那么,这该怎么办呢?

答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。

可以通过如下pom文件引入该jar包:

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>transmittable-thread-local</artifactId>
   <version>2.11.0</version>
   <scope>compile</scope>
</dependency>
private static void fun2() throws Exception {
    TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("父线程获取数据:" + threadLocal.get());

    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    threadLocal.set(6);
    ttlExecutorService.submit(() -> {
        System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
    });

    threadLocal.set(7);
    ttlExecutorService.submit(() -> {
        System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
    });

}
结果:
父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:7

如果你仔细观察这个例子,你可能会发现,代码中除了使用TransmittableThreadLocal类之外,还使用了TtlExecutors.getTtlExecutorService方法,去创建ExecutorService对象。

这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。

创建ExecutorService对象,底层的submit方法会TtlRunnable或TtlCallable对象。

以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run方法:

public void run() {
    Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
    if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
        Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);

        try {
            this.runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    } else {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
}

这段代码的主要逻辑如下:

把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。
执行真正的run方法,可以获取到父类最新的ThreadLocal数据。
从备份的数据中,恢复当时的ThreadLocal数据。

问题10
ThreadLocal有哪些用途?
下面列举几个常见的场景:

1、在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
2、在hiberate中管理session。
3、在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
4、获取当前登录用户上下文。
5、临时保存权限数据。
6、使用MDC保存日志信息。
等等,还有很多业务场景,这里就不一一列举了。

传递用户信息

package com.example.study.interceptor;

public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);


    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {

        Cookie[] cookieList = request.getCookies();
        if (cookieList == null) {
            return false;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookieList.length; i++) {
                if (cookieList[i].getName().equals("userToken")) {
                    retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            logger.error("Cookie Decode Error.", e);
        }

        if (retValue  == null) {
            // token为空,用户未登录
            CurrentSysUser.removeCurrentSysUser();
            response.sendRedirect("login");
            return false;
        }
        String userId = redis.get(retValue);
        if (userId == null) {
            // operator为空,用户登录超时
            CurrentSysUser.removeCurrentSysUser();
            response.sendRedirect("login");
            return false;
        }
        SysUser sysUser = new SysUser();
        BeanUtils.copyProperties(sysUser, operator);
        sysUser.setUserAccount(operator.getUserCode());
        sysUser.setCompanyId(operator.getCompanyCode());
        sysUser.setCompanyName(operator.getCompanyName());
        sysUser.setSysPositionId(operator.getRoleCode());
        sysUser.setSysPositionName(operator.getRoleName());
        sysUser.setOrgId(operator.getOrgCode());
        RoleWorkData workData = operator.getRole().getWorkData();
        HashSet<String> dataSet = workData.getStrDataSet(RoleWorkData.DATATYPE_DEMAND_SIDE);
        sysUser.setDemandSide(dataSet);
        sysUser.setSaleChannel(workData.getStrDataSet(RoleWorkData.DATATYPE_SALE_CHANNEL));
        sysUser.setPopShopCodes(workData.getStrDataSet(RoleWorkData.DATATYPE_POP_SHOP_CODE));
        // 用户已登录
        CurrentSysUser.setCurrentSysUser(sysUser);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        if (ex != null) {
            response.setStatus(500);
            logger.error("system.error", ex);
        }
        CurrentSysUser.removeCurrentSysUser();
    }

}

public class CurrentSysUser {
	
	
	private static ThreadLocal<SysUser> sysUsers = new ThreadLocal<SysUser>();
	
	public static void setCurrentSysUser(SysUser sysUser){
		sysUsers.set(sysUser);
	}
	
	public static SysUser getCurrentSysUser(){
		return sysUsers.get();
	}
	
	public static void removeCurrentSysUser(){
		sysUsers.remove();
	}
	
	public static String getCurrentSysUserAccount(){
		return sysUsers.get().getUserAccount();
	}
}

Alibaba开源的TTL
父子线程继承
https://mp.weixin.qq.com/s/WDp4JmwdfHQF-v8dnvJCFg

posted @ 2021-07-09 15:37  倔强的老铁  阅读(175)  评论(0编辑  收藏  举报