ThreadLocal深入理解与内存泄露分析

ThreadLocal

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。可以理解为如下三点:

1、每个线程都有自己的局部变量

每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的。

2、独立于变量的初始化副本

ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。

3、状态与某一个线程相关联

ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的私有状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

ThreadLocal的接口方法

    public T get() { }
    public void set(T value) { }
    public void remove() { }
    protected T initialValue() { }  

get()用来获取当前线程中变量的副本(保存在ThreadLocal中)。

set()用来设置当前线程中变量的副本。

remove()用来移除当前线程中变量的副本。

initialValue()是一个protected方法,用来给ThreadLocal变量提供初始值,每个线程都会获取这个初始值的一个副本。

使用示例

public class ThreadLocalTest {
        //创建一个Integer型的线程本地变量
        public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return 0;
            }
        };
        //计数
        static class Counter implements Runnable{
            @Override
            public void run() {
                //获取当前线程的本地变量,然后累加100次
                int num = local.get();
                for (int i = 0; i < 100; i++) {
                    num++;
                }
                //重新设置累加后的本地变量
                local.set(num);
                System.out.println(Thread.currentThread().getName() + " : "+ local.get());
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[5];
            for (int i = 0; i < 5; i++) {
                threads[i] = new Thread(new Counter() ,"CounterThread-[" + i+"]");
                threads[i].start();
            }
        }
    }  

输出:

CounterThread-[2] : 100

CounterThread-[0] : 100

CounterThread-[3] : 100

CounterThread-[1] : 100

CounterThread-[4] : 100

对initialValue函数的正确理解

public class ThreadLocalMisunderstand {  

    static class Index {
        private int num;
        public void increase() {
            num++;
        }
        public int getValue() {
            return num;
        }
    }
    private static Index num=new Index();
    //创建一个Index型的线程本地变量
    public static final ThreadLocal<Index> local = new ThreadLocal<Index>() {
        @Override
        protected Index initialValue() {
            return num;
        }
    };
    //计数
    static class Counter implements Runnable{
        @Override
        public void run() {
            //获取当前线程的本地变量,然后累加10000次
            Index num = local.get();
            for (int i = 0; i < 10000; i++) {
                num.increase();
            }
            //重新设置累加后的本地变量
            local.set(num);
            System.out.println(Thread.currentThread().getName() + " : "+ local.get().getValue());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(new Counter() ,"CounterThread-[" + i+"]");
        }
        for (int i = 0; i < 5; i++) {
            threads[i].start();
        }
    }
}

输出:

CounterThread-[0] : 12019

CounterThread-[2] : 14548

CounterThread-[1] : 13271

CounterThread-[3] : 34069

CounterThread-[4] : 34069

现在得到的计数不一样了,并且每次运行的结果也不一样,说好的线程本地变量呢?

之前提到,我们通过覆盖initialValue函数来给我们的ThreadLocal提供初始值,每个线程都会获取这个初始值的一个副本。而现在我们的初始值是一个定义好的一个对象,num是这个对象的引用。换句话说我们的初始值是一个引用。引用的副本和引用指向的不就是同一个对象吗?

如果我们想给每一个线程都保存一个Index对象应该怎么办呢?那就是创建对象的副本而不是对象引用的副本。

    private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
        @Override
        protected Index initialValue() {
            return new Index(); //注意这里,新建一个对象
        }
    };  

ThreadLocal源码分析

存储结构

public class Thread implements Runnable {
......
    ThreadLocal.ThreadLocalMap threadLocals = null;//一个线程对应一个ThreadLocalMap
......
}
public class ThreadLocal<T> {
......
    static class ThreadLocalMap {//静态内部类
        static class Entry extends WeakReference<ThreadLocal> {//键值对
            //Entry是ThreadLocal对象的弱引用,this作为键(key)
            /** The value associated with this ThreadLocal. */
            Object value;//ThreadLocal关联的对象,作为值(value),也就是所谓的线程本地变量

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ......
        private Entry[] table;//用数组保存所有Entry,采用线性探测避免冲突
    }
......
}

ThreadLocal源码

public class ThreadLocal<T> {
    /**ThreadLocals rely on per-thread linear-probe hash maps attached to each thread
       (Thread.threadLocals and inheritableThreadLocals).
       The ThreadLocal objects act as keys, searched via threadLocalHashCode.  */
    //每个线程对应一个基于线性探测的哈希表(ThreadLocalMap类型),通过Thread类的threadLocals属性关联。
    //在该哈希表中,逻辑上key为ThreadLocal,实质上通过threadLocalHashCode属性作为哈希值查找。
    private final int threadLocalHashCode = nextHashCode();

    /**  The next hash code to be given out. Updated atomically. Starts at zero.*/
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    /**Returns the next hash code.*/
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**Returns the current thread's "initial value" for this thread-local variable.
       This method will be invoked the first time a thread accesses the variable with the {@link #get} method,
       unless the thread previously invoked the {@link #set} method, in which case the <tt>initialValue</tt>
       method will not be invoked for the thread.  Normally, this method is invoked at most once per thread,
       but it may be invoked again in case of subsequent invocations of {@link #remove} followed by {@link #get}.

       <p>This implementation simply returns <tt>null</tt>; if the programmer desires thread-local variables
       to have an initial value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be subclassed,
       and this method overridden.  Typically, an anonymous inner class will be used.
     * @return the initial value for this thread-local
     */
    //初始化线程本地变量,注意上面讲到的关于该方法的正确理解
    protected T initialValue() {
        return null;
    }

    /**  Creates a thread local variable.*/
    public 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);//获取当前线程对象关联的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//以this作为key,查找线程本地变量
            if (e != null)//如果该线程本地变量已经存在,返回即可
                return (T)e.value;
        }
        return setInitialValue();//如果该线程本地变量不存在,设置初始值并返回
    }

    /**Variant of set() to establish initialValue.
       Used instead of set() in case user has overridden the set() method.
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();//获取线程本地变量的初始值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)//如果当前线程关联的ThreadLocalMap已经存在,将线程本地变量插入哈希表
            map.set(this, value);
        else
            createMap(t, value);//否则,创建新的ThreadLocalMap并将<this,value>组成的键值对加入到ThreadLocalMap中
        return value;
    }

    /**Sets the current thread's copy of this thread-local variableto the specified value.
       Most subclasses will have no need to override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**Removes the current thread's value for this thread-local variable.
       If this thread-local variable is subsequently {@linkplain #get read} by the current thread,
       its value will be reinitialized by invoking its {@link #initialValue} method,
       unless its value is {@linkplain #set set} by the current thread in the interim.
       This may result in multiple invocations of the <tt>initialValue</tt> method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {//从当前线程的ThreadLocalMap中移除线程本地变量
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

    /** Get the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.*/
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**Create the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.*/
    //创建ThreadLocalMap后与当前线程关联,并将线程本地变量插入ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    //其他
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

    static class ThreadLocalMap {//基于线性探测解决冲突的哈希映射表
    ......
    }
......
}

内存泄露与WeakReference

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

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

对于键值对Entry,key为ThreadLocal实例,value为线程本地变量。不难发现,Entry继承自WeakReference<ThreadLocal>。WeakReference就是所谓的弱引用,也就是说Key是一个弱引用(引用ThreadLocal实例)。

关于强引用、弱引用,参看:http://blog.csdn.net/sunxianghuang/article/details/52267282

public class Test {
    public static class MyThreadLocal extends ThreadLocal {
        private byte[] a = new byte[1024*1024*1];

        @Override
        public void finalize() {
            System.out.println("My threadlocal 1 MB finalized.");
        }
    }

    public static class My50MB {//占用内存的大对象
        private byte[] a = new byte[1024*1024*50];

        @Override
        public void finalize() {
            System.out.println("My 50 MB finalized.");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocal tl = new MyThreadLocal();
                tl.set(new My50MB());

                tl=null;//断开ThreadLocal的强引用
                System.out.println("Full GC 1");
                System.gc();

            }

        }).start();
        System.out.println("Full GC 2");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 3");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 4");
        System.gc();
        Thread.sleep(1000);

    }
}

输出:

Full GC 2

Full GC 1

My threadlocal 1 MB finalized.

Full GC 3

My 50 MB finalized.

Full GC 4

从输出可以看出,一旦threadLocal的强引用断开,key的内存就可以得到释放。只有当线程结束后,value的内存才释放。

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指threadlocal实例,所以threadlocal将会被gc回收。但是,我们的value却不能回收,因为存在一条从current
thread连接过来的强引用。

只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

注: 实线代表强引用,虚线代表弱引用.

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露。但是value在threadLocal设为null线程结束这段时间不会被回收,就发生了我们认为的“内存泄露”。

因此,最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。  

为了最小化内存泄露的可能性和影响,在ThreadLocal的get,set的时候,遇到key为null的entry就会清除对应的value。

所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,或者get,set方法调用时依然没有遇到key为null的entry,那么这个期间就会发生真正的内存泄露。

使用ThreadLocal需要注意,每次执行完毕后,要使用remove()方法来清空对象,否则 ThreadLocal 存放大对象后,可能会OMM。

为什么使用弱引用

To help deal with very large and long-lived usages, the hash table entries use  WeakReferences for keys.

应用实例

Hibernate中使用Threadlocal实现线程相关的Session

参考:

JDK 1.7源码

http://my.oschina.net/xpbug/blog/113444

时间: 2024-08-05 04:42:11

ThreadLocal深入理解与内存泄露分析的相关文章

内存泄露分析之MAT工具简单使用

使用了Heap视图的方式来分析内存泄露之后,我们尝试用MAT插件来分析下. MAT,提供了太强大的功能,以至于在测试的过程中也是懵懂的,没有彻底的研究. 1. 安装Android Sdk,Java SDK,Eclipse之类的软件之后, 2. 安装Eclipse MAT插件 3. 调出DDMS的Heap视图 4. 手机连接电脑之后,选择要测试的进程,并点击Heap 5. 在手机上操作需要测试的功能 6. 选择Dump HPROF file功能. 7. 如果Eclipse中直接安装了MAT插件之后

学会用Clang来进行内存泄露分析

最近项目出现了内存泄露的问题,对于PC x86平台来说,一点点的内存泄露往往不会出错,很难进行debug调试.这个时候我们可以用到苹果给我们带来的神器--Clang编译器来进行内存泄露分析检测,开关打开之后,生成出来的二进制文件对内存泄露的敏感程度非常高,只要有内存泄露基本就会立马停止并进行报错. 由于项目是用CMake进行组织,因此使用CMake的方法来进行开关的打开,首先要让CC和CXX都变成Clang和Clang++(注意:在Clang下有时候会对inline函数报错,需要将inline去

ThreadLocal是否会引发内存泄露的分析(转)

这篇文章,主要解决一下疑惑: 1. ThreadLocal.ThreadLocalMap中提到的弱引用,弱引用究竟会不会被回收? 2. 弱引用什么情况下回收? 3. JAVA的ThreadLocal和在什么情况下会内存泄露? 带着这些疑问,自己模拟了一下ThreadLocal.ThreadLocalMap的结构,先展示下自己涉及的结构: 自己实现一个simple的ThreadLocalMap,里面用一个entry用来存放由自己模拟的ThreadLocal调用set方法set进去的值. 并且和JD

Android内存泄露分析简要思路

工作中遇到挺多需要分析内存泄露问题的情况,现在大致简要写下思路,等之后时间相对比较充裕再进行补充. 1.明白内存泄露的判断依据? 个人总结为:持续增加,只增不减! 理解一下这8个字,配合几个命令和工具来确定一下你的应用是否存在内存泄露问题,这是很关键的,如果一开始就判断错误了,那么没有继续往下进行的理由. 命令如下: adb shell dumpsys meminfo 应用包名 [当然,比较粗略地话,可以用adb shell procrank] 这时候你可以看到一个内存使用情况表 而我们首先关注

ThreadLocal可能引起的内存泄露

threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法. 在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用. 每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个thread

JAVA内存泄露分析及解决

一,问题产生     项目采用Tomcat6.0为服务器,数据库为mysql5.1,数据库持久层为hibernate3.0,以springMVC3.0为框架,项目开发完成后,上线前夕进行稳定性拷机,测试数据为插入4条/S,更新4条/S,访问300次/S,前期运行速度顺畅,三天后就开始运行缓慢,访问量达到1500W次后以抛出Java heap space结束. 二.问题分析     1.前期分析为连接池内存溢出,期间优化了连接池参数,调整了tomcat线程参数,替换数据库连接池,问题依旧     

JVM内存管理概述与android内存泄露分析

一.内存划分 将内存划分为六大部分,分别是PC寄存器.JAVA虚拟机栈.JAVA堆.方法区.运行时常量池以及本地方法栈. 1.PC寄存器(线程独有):全称是程序计数寄存器,它记载着每一个线程当前运行的JAVA方法的地址, 如果是当前执行的是本地方法,则程序计数器会是一个空地址.它的作用就是用来支持多线程,线程的阻塞.恢复. 挂起等一系列操作,直观的想象一下,要是没有记住每个线程当前运行的位置,又如何恢复呢.依据这一点, 每一个线程都有一个PC寄存器,也就是说PC寄存器是线程独有的. 2.JAVA

【转】.. Android应用内存泄露分析、改善经验总结

原文网址:http://wetest.qq.com/lab/view/107.html?from=ads_test2_qqtips&sessionUserType=BFT.PARAMS.194206.TASKID&ADUIN=554147273&ADSESSION=1467939955&ADTAG=CLIENT.QQ.5479_.0&ADPUBNO=26582 前言   通过这几天对好几个应用的内存泄露检测和改善,效果明显: 完全退出应用时,手动触发GC,从原来占有

JAVA简单内存泄露分析及解决

一.问题产生    项目采用Tomcat6.0为服务器,数据库为mysql5.1,数据库持久层为hibernate3.0,以springMVC3.0为框架,项目开发完成后,上线前夕进行稳定性拷机,测试数据为插入4条/S,更新4条/S,访问300次/S,前期运行速度顺畅,三天后就开始运行缓慢,访问量达到1500W次后以抛出Java heap space结束.二.问题分析    1.前期分析为连接池内存溢出,期间优化了连接池参数,调整了tomcat线程参数,替换数据库连接池,问题依旧    2.看来