探索JAVA并发 - ThreadLocal

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

从SimpleDateFormat说起

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

SimpleDateFormat用法

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容器被各种共享,一不小心就会写成下面这种样子。

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线程不安全分析

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

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就有用武之地了。

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源码分析

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对象的结构再做个简单了解。

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用的。

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初始化时,

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方法使其清除。


 

最佳阅读体验请点击文末 阅读原文

640?wx_fmt=jpeg

↑长按二维码添加关注↑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值