ThreadLocal辨析

为什么要用ThreadLocal?

        ThreadLocal 和 Synchonized 都用于解决多线程并发访问。可是 ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。而 ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间访问的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

ThreadLocal的使用:

ThreadLocal 类接口很简单,主要有 4 个方法:

• void set(Object value) 设置当前线程的线程局部变量的值。

• public Object get() 该方法返回当前线程所对应的线程局部变量。

• public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

• protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法,是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。

举例:

public final static ThreadLocal RESOURCE = new ThreadLocal();

上面的RESOURCE代表一个能够存放String类型的ThreadLocal对象。 此时不论一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

源码分析:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        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;
            }
        }
        return setInitialValue();
    }



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

1.首先进入get方法,可以看到上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型成员,所以 getMap 是直接返回 Thread 的成员。

2.ThreadLocal 的内部类 ThreadLocalMap 源码:

 

可以看到Entry构造方法中参数类似于map的key、value结构,key就是ThreadLocal本身,value就是需要隔离访问的变量。同时table是Entry[]类型,表示用数组保存Entry,因为可能有多个变量需要线程隔离访问。

Entry 内部静态类,它继承了 WeakReference(弱引用),记录了两个信息,一个是 ThreadLocal类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。回到get 方法,其实就是拿到每个线程独有的 ThreadLocalMap 然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初始化等工作。

 

引发的内存泄露分析:

1.有如下代码示例,首先将堆内存大小设 置为-Xmx256m,注释掉使用ThreadLocal的代码:

public class ThreadLocalOOM {
    private static final int TASK_LOOP_SIZE = 100;

    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
    }

    ThreadLocal<LocalVariable> localVariable;
            //= new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    //ThreadLocalOOM oom = new ThreadLocalOOM();
                    //oom.localVariable = new ThreadLocal<>();
                    //oom.localVariable.set(new LocalVariable());
                    System.out.println("use local varaible");
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

}

当所有的任务提交执行完成 后,可以看见,应用的内存占用基本上为 25M 左右 :

2.简单的在每个任务中 new 出一个数组,执行完成后我们 可以看见,内存占用基本和场景 1 相同

3.使用ThreadLocal后,内存变为100MB那么多:

4.再加入一行remove代码,可以看见和1的情况很像:

分析:

仔细观察 ThreadLocalMap,这个 map 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在发生GC时会被回收掉:如图所示

图中的虚线表示弱引用,当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,线程就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前 线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。

源码分析:

通过进入set和get方法中,无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。

 我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,而不是因为弱引用导致的。

 总结

JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。调用 remove、get、set 方法的时候,回收弱引用。 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。

 

 

 

 

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值