前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >探索JAVA并发 - ThreadLocal

探索JAVA并发 - ThreadLocal

作者头像
acupt
发布2019-08-26 15:38:22
3510
发布2019-08-26 15:38:22
举报
文章被收录于专栏:一杯82年的JAVA一杯82年的JAVA

使用ThreadLocal可以维持线程封闭性,使线程中的某个值与保存值的对象关联,防止对可变的单例变量或全局变量进行共享,但使用不当也会造成内存泄漏,先了解它,再使用它。

从SimpleDateFormat说起

SimpleDateFormat是我们常用的日期格式化工具,但熟悉的朋友都知道它是线程不安全的。

SimpleDateFormat用法

代码语言:javascript
复制
public class Acuptest {

    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
        System.out.println(sdf.format(new Date()));
    }
}

SimpleDateFormat线程不安全场景

上面的用法完全没有问题,但现在spring无处不在,很多类都是以bean的形式存在于spring容器被各种共享,一不小心就会写成下面这种样子。

代码语言:javascript
复制
public class Acuptest {

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");

    public String format(Date date) {
        return sdf.format(date);
    }
}

只是这样还看不出什么问题,但既然提到了SimpleDateFormat是线程不安全的,那么就看看为什么不安全。

SimpleDateFormat线程不安全分析

进入源码,只看关键部分。

代码语言:javascript
复制
public abstract class DateFormat extends Format {

    // 一个成员变量
    protected Calendar calendar;

    // 一个抽象方法
    public abstract StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition);

    // 提供给外部使用的方法
    public final String format(Date date){
        return format(date, new StringBuffer(),
                      DontCareFieldPosition.INSTANCE).toString();
    }
}

public class SimpleDateFormat extends DateFormat {

    // 实现了父类的抽象方法
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos){
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
        // 到这里就能发现问题了,竟然给成员变量设置成了传进来的参数
        // 在并发情况下calendar的值就不可信了,可能线程A前脚刚设置完准备执行下一条语句,线程B紧随其后就把值给改了
        // Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        // 略
    }
}

SimpleDateFormat线程安全用法

使用局部变量

只要不让多线程访问同一个对象,每次要用就new一个对象即可。

使用ThreadLocal

很多时候某些对象往往不适合频繁创建、销毁,但它又像SimpleDateFormat那样线程不安全。这时候ThreadLocal就有用武之地了。

代码语言:javascript
复制
public class Acuptest {

    // 为每个线程单独分配一个SimpleDateFormat,线程内部可以复用,线程之间不能共享。
    private ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            // get()方法获取不到当前线程的SimpleDateFormat对象时,会调用此方法创建一个并绑定到线程
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
        }
    };

    public String format(Date date) {
        return sdf.get().format(date);
    }

ThreadLocal源码分析

代码语言:javascript
复制
public class ThreadLocal<T> {
    //...
    // 获取当前线程绑定的对象,如果没有,将调用initialValue生成一个并绑定
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 从当前线程中取到一个MAP
        // key: ThreadLocal
        // value: ThreadLocal的泛型 <T>
        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;
            }
        }
        // Thread对象可能还没创建ThreadLocalMap成员变量
        // 或者ThreadLocalMap里没有当前ThreadLocal对象对应的<T>值
        // 此时需要设置初始值
        return setInitialValue();
    }

    // 获取线程里的MAP
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 设置初始值
    private T setInitialValue() {
        // 创建一个新的对象
        T value = initialValue();
        // 重新获取当前线程,因为没有参数接收线程信息
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value); // 设置初始值
        else
            createMap(t, value); // 创建MAP并设置初始值
        return value;
    }

    // 初始化一个对象,默认返回null,可在使用时重写此方法
    protected T initialValue() {
        return null;
    }
    // ...
}

Thread源码分析

上面的源码中看到ThreadLocal多次使用Thread中的成员变量threadLocals,于是对Thread对象的结构再做个简单了解。

代码语言:javascript
复制
public class Thread implements Runnable {

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

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

    // 略
}

public class ThreadLocal<T> {

    static class ThreadLocalMap {

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

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

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
    }
}

threadLocals和inheritableThreadLocals

从Thread源码中可以看到ThreadLocal.ThreadLocalMa类型的成员变量有两个,有个是之前没有见过的inheritableThreadLocals,这个变量不是给ThreadLocal用的,而是给另一个类似的工具InheritableThreadLocal用的。

代码语言:javascript
复制
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

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

从源码上看,InheritableThreadLocal继承了ThreadLocal,然后使用的MAP换了,其他就没什么特别的。

但InheritableThreadLocal有着特殊的功能:它可以使用父线程的inheritableThreadLocals变量,实现父子线程共享变量。

InheritableThreadLocal为什么可以让子线程使用父线程的变量,关键的地方不在它,而在Thread类的初始化流程,Thread初始化时,

代码语言:javascript
复制
public class Thread implements Runnable {

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 略
        Thread parent = currentThread();
        // 略
        // inheritThreadLocals默认为true
        // 父线程inheritableThreadLocals不为空则复制一份
        // 值复制,非引用复制
        // 只是复制父线程当前拥有的对象
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // 略
    }
}

ThreadLocal中的弱引用(WeakReference)

从上面的源码中注意到: ThreadLocal.ThreadLocalMap.Entry extends WeakReference<threadlocal</threadlocal

Entry的key是ThreadLocal,是个弱引用(被GC扫描到就回收)。如果不这样,当ThreadLocal用完了,但线程还没结束,因此Thread里面还持有着ThreadLocal的强引用,那么它永远不会被回收,可以认为内存泄漏了。

ThreadLocal的内存泄漏

就算是使用了弱引用,依然存在内存泄漏的可能。因为弱引用仅仅是Entry的key(ThreadLocal),value(泛型T)并不是弱引用。最终可能出现的结果就是,ThreadLocal被回收了,Thread里的MAP中KEY就没了,但value还在,这样一来这个value永远不会被get()方法返回,确又存在于内存不愿消散。

内部实现尽量避免内存泄漏:

在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

如果没有调用这些方法去触发这个过程,依然会内存泄漏,所以在线程用完这个对象后,可以显示调用remove方法使其清除。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一杯82年的JAVA 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从SimpleDateFormat说起
    • SimpleDateFormat用法
      • SimpleDateFormat线程不安全场景
        • SimpleDateFormat线程不安全分析
          • SimpleDateFormat线程安全用法
            • 使用局部变量
            • 使用ThreadLocal
        • ThreadLocal源码分析
        • Thread源码分析
          • threadLocals和inheritableThreadLocals
            • ThreadLocal中的弱引用(WeakReference)
              • ThreadLocal的内存泄漏
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档