ThreadLocal类(线程本地存储)详解

Java编程之路 同时被 3 个专栏收录
119 篇文章 14 订阅
107 篇文章 4 订阅

本文包含知识点

  • ThreadLocal是什么?
  • ThreadLocal使用场景
  • ThreadLocal实现原理及核心方法
  • ThreadLocal使用注意事项

books 1.ThreadLocal是什么?

从广义上来说实现线程安全的方式包括:互斥同步、非阻塞同步、无同步方案三种,ThreadLocal就属于无同步方案中的一种。

ThreadLocal被称为线程本地存储,顾名思义就将共享的数据存储到每个线程本地,这样每个线程拥有的都是该共享数据的副本,以此来限制共享数据的可见范围或可变性。使用ThreadLocal不需要锁也实现了线程安全且效率比互斥同步高,某些情况下也能避免类方法参数层层传递的缺点。比如spring框架中的RequestContextHolder类(每个请求中的request由于线程隔离了,信息都不一样)就使用ThreadLocal来实现的。

books 2.ThreadLocal使用场景

  • 每个线程需要独享数据,或者说需要限制共享数据的可见范围,比如常用的工具类SimpleDateFormat就是线程不安全的,可以和ThreadLocal来搭配使用
  • 每个线程需要保存"全局"数据,例如我们项目中实际用到的:过滤器过滤到所有接口的入参token放在ThreadLocal,减少token参数层层传递,方便调用

下面以SimpleDateFormat为例进行测试第一个使用场景:每个线程独享数据

public class DateFormatUitls {
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(int milSecond){
        Date date = new Date(milSecond * 50000);
        return sdf.format(date);
    }
}

public class ThreadLocalTest {
    static ExecutorService es = Executors.newFixedThreadPool(10);
    
    public static void main(String[] args) {
        try {
            for (int i = 0; i < 1000; i++) {
                int milSecond = i;
                es.submit(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(DateFormatUitls.format(milSecond));
                    }
                });
            }
        }catch (Exception e){
        }finally{
            es.shutdown();
        }
    }
}

并发情况下存在多线程共享SimpleDateFomat对象的情况,输出结果中可以看到存在相同的时间点,也就是存在并发问题,所以我们可以使用ThreadLocal来改造解决(对sdf.format()方法加锁同步也是可以的,考虑效率问题):

public class DateFormatUitls {

    public static String format(int milSecond){
        Date date = new Date(milSecond * 1000);
        return ThreadLocalSimpleDateFormat.threadLocal.get().format(date);
    }
}

class ThreadLocalSimpleDateFormat extends ThreadLocal{

   static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
}

public class ThreadLocalTest {
    static ExecutorService es = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 1000; i++) {
                int milSecond = i;
                es.submit(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(DateFormatUitls.format(milSecond));
                    }
                });
            }
        }catch (Exception e){
        }finally{
            es.shutdown();
        }
    }
}

接着测试第二个常用场景:每个线程保存全局数据

这里以本人项目中实际用到的存储全局token为例(代码经过大量精简):

/**
 * @author simons.fan
 * @description 过滤器进行鉴权和授权
 **/
@Component
@WebFilter(urlPatterns = "/**")
public class SecutityFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //TODO
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String token = servletRequest.getParameter("token");
        //TODO省略参数校验……
        //将token写入ThreadLocal中
        TokenContextHolder.set(StringUtils.isEmpty(token) ? "默认值" : token);
        //TODO省略鉴权……
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        //TODO
    }
}
/**
 * @author simons.fan
 * @description 全局ThreadLocal类型的token容器
 **/
public class TokenContextHolder {

    public static ThreadLocal<String> tokenContext = new ThreadLocal();

    /**
     * 把token放入threadLocal中
     **/
    public static void set(String token) {
        tokenContext.set(token);
    }

    /**
     * 从threadLocal中获取对应线程存储的值
     **/
    public static String get() {
        return tokenContext.get();
    }
}

后续需要获取token的任意接口(均可直接通过ThreadLocal获取即可,而不需要层层传递参数,且token不存在线程安全问题,每个token都是对应线程set进去的注意:在链路的末端位置要及时调用ThreadLocal的remove()方法

books 3.ThreadLocal实现原理及核心方法

关于ThreadLocal原理,首先要搞懂Thread和ThreadLocal、ThreadLocalMap三者间的关系。我们说每个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以变量为值的KEY_VALUE值对,ThreadLocal对象就是当前线的ThreadLocalMap的访问入口,每个ThreadLocal对象都包含了一个唯一的threadLocalHashCode值,使用这个key可以方便快速的获取到存储在ThreadLocal中的本地变量值。那么既然ThreadLocalMap是个map(数组结构),就会存在hash冲突的问题,当实际发生hash冲突后,ThreadLocalMap采用的是线性探测法(继续寻找下一个空位置,而不是用链表结构)【注:这个结论来自于慕课网课程:玩转Java并发工具,精通JUC,成为并发多面手 学习所得

Thread、ThreadLocal、ThreadLocalMap关系图

ThreadLocal核心方法

  • initialValue():用于初始化本地线程变量值,需要显式重写,不重写的话会返回默认值null,该方法会延迟加载(在调用get()方法时候才会被触发)
  • set(T var1):手动为threadLocal设置值,当ThreadLocal显式set值后,不会再调用initialValue()方法初始化。本质上set和initialValue方式最终都是通过threadLocalMap.set()方法进行操作的
  • get():获取TreadLocal对应的值
  • remove():从ThreadLocalMap中删除这个key-value
//ThreadLocal的get()方法
public T get() {
	//得到当前线程
	Thread var1 = Thread.currentThread();
	//得到当前线程的ThreadLocalMap对象
	ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
	//如果ThreadLocal没有被显式set()过,则调用initialValue()先初始化
	if (var2 != null) {
		ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
		if (var3 != null) {
			Object var4 = var3.value;
			return var4;
		}
	}
	//调用initialValue()先初始化
	return this.setInitialValue();
}

//ThreadLocal的set()方法
public void set(T var1) {
	Thread var2 = Thread.currentThread();
	ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
	if (var3 != null) {
		//说明不是第一次set,直接覆盖,this表示ThreadLocal
		var3.set(this, var1);
	} else {
		//说明是第一次set,新建map
		this.createMap(var2, var1);
	}
}

//ThreadLocal的remove()方法
public void remove() {
	ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
	if (var1 != null) {
		//删除this对应的threadLocal的value
		var1.remove(this);
	}
}

books 4.ThreadLocal使用注意事项

使用threadLocal不当可能导致内存泄露,谈到这个问题需要先回顾一下内存泄露和弱引用的概念:

  • 内存泄露:对象不再使用但是内存空间却没有释放
  • 对象四种引用:

    1.强引用:指代码中普遍存在的,类似于Object obj = new Object()的这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;

    2.软引用:描述一些还有用但是非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常前会把这些对象列入回收范围之中进行第二次回收,如果第二次回收还是没有足够的内存,才会抛出内存溢出异常;

    3.弱引用:描述非必需对象的,强度比软引用还弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,下一次垃圾收集无论内存是否足够都会回收掉只被弱引用关联的对象;

    4.虚引用:不会对对象的生存时间构成影响,也无法通过虚引用来获取一个对象实例;

上面说到ThreadLocal本质上就是操作的ThreadLocalMap,我们进入ThreadLocalMap源码看下:

key是弱引用类型

也就是说每个Entry的key都是弱引用类型,value是强引用类型,在正常情况下执行业务的工作线程执行完,可以被正常GC掉,包括key和value;但是如果该线程一直未终止,例如线程池里的常驻线程一直不结束,那么这个key对应的value就一直不能被回收,因为key是弱引用已经被GC,但是value由于是强引用一直不能被GC,导致OOM。简单的GCROOT引用链如下:Thread-->TreadLocalMap-->Entry(null,value)-->value,即Thread和value之间一直存在强引用关系

JDK设计者已经考虑到这个问题,在ThreadLocal的set()/remove()等方法中,如果Entry的key为null,则会把value也设置成null来帮助value的GC。但由于工作线程一直在,我们的业务又已经处理完毕,所以一般不会再去显式的调用这些方法。

  • 正确的姿势应该在业务方法的末端显式的调用remove()方法,删除对应的key-value
  • ThreadLocal的get()方法必须和initialValue()方法的返回值或set(T var1)方法的泛型相对应

最后就是要结合具体业务场景,权衡使用锁,还是使用ThreadLocal或其它方式,不能为了使用ThreadLocal而用ThreadLocal。


books 引申阅读:java线程池详解

  • 2
    点赞
  • 3
    评论
  • 10
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页

打赏

饭一碗

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值