• ThreadLocal源码分析及内存泄露预防


    ThreadLocal是什么?

      ThreadLocal是啥?以前面试别人时就喜欢问这个,有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢?

      ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

      通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。概括起来说,ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    API说明

    1、ThreadLocal()

      创建一个线程本地变量。

    2、T get()

       返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。

    3、protected  T initialValue()

       返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

       若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

    4、void remove()

      移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。

    5、void set(T value)

      将此线程局部变量的当前线程副本中的值设置为指定值。

    ThreadLocal使用示例

    假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了

    package concurrent;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * Created by chenhao on 2018/12/03.
     */
    public class ThreadLocalDemo {
        public static void main(String []args){
            for(int i=0;i<5;i++){
                final Thread t = new Thread(){
                    @Override
                    public void run(){
                        System.out.println("当前线程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get());
                    }
                };
                t.start();
            }
        }
        static   class ThreadId{
            //一个递增的序列,使用AtomicInger原子变量保证线程安全
            private static final AtomicInteger nextId = new AtomicInteger(0);
            //线程本地变量,为每个线程关联一个唯一的序号
            private static final ThreadLocal<Integer> threadId =
                    new ThreadLocal<Integer>() {
                        @Override
                        protected Integer initialValue() {
                            return nextId.getAndIncrement();//相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
                        }
                    };
    
           //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
            public static int get() {
                return threadId.get();
            }
        }
    }

    运行结果:

    当前线程:Thread-4,已分配ID:1
    当前线程:Thread-0,已分配ID:0
    当前线程:Thread-2,已分配ID:3
    当前线程:Thread-1,已分配ID:4
    当前线程:Thread-3,已分配ID:2

    ThreadLocal源码分析

      ThreadLocal最常见的操作就是set、get、remove三个动作,下面来看看这三个动作到底做了什么事情。首先看set操作,源码片段

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    第 2 行代码取出了当前线程 t,然后调用getMap(t)方法时传入了当前线程,换句话说,该方法返回的ThreadLocalMap和当前线程有点关系,我们先记录下来。进一步判定如果这个map不为空,那么设置到Map中的Key就是this,值就是外部传入的参数。这个this是什么呢?就是定义的ThreadLocal对象。
    代码中有两条路径需要追踪,分别是getMap(Thread)和createMap(Thread , T)。首先来看看getMap(t)操作

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

    在这里,我们看到ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中的定义是:

    ThreadLocal.ThreadLocalMap threadLocals = null;

    即:每个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量,ThreadLocal.ThreadLocalMap是一个ThreadLocal类的静态内部类(如下所示),所以Thread类可以进行引用.所以每个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用

    static class ThreadLocalMap {

    首先获取当前线程的引用,然后获取当前线程的ThreadLocal.ThreadLocalMap对象,如果该对象为空就创建一个,如下所示:

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

    这个this变量就是ThreadLocal的引用,对于同一个ThreadLocal对象每个线程都是相同的,但是每个线程各自有一个ThreadLocal.ThreadLocalMap对象保存着各自ThreadLocal引用为key的值,所以互不影响,而且:如果你新建一个ThreadLocal的对象,这个对象还是保存在每个线程同一个ThreadLocal.ThreadLocalMap对象之中,因为一个线程只有一个ThreadLocal.ThreadLocalMap对象,这个对象是在第一个ThreadLocal第一次设值的时候进行创建,如上所述的createMap方法.

    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

    至此,ThreadLocal的原理我们应该已经清楚了,简单来讲,就是每个Thread里面有一个ThreadLocal.ThreadLocalMap threadLocals作为私有的变量而存在,所以是线程安全的。ThreadLocal通过Thread.currentThread()获取当前的线程就能得到这个Map对象,同时将自身(ThreadLocal对象)作为Key发起写入和读取,由于将自身作为Key,所以一个ThreadLocal对象就能存放一个线程中对应的Java对象,通过get也自然能找到这个对象。

    最后来看看get()、remove()代码,或许看到这里就可以认定我们的理论是正确的

    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();
    }
    
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
    }

    第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是  this,而不是当前线程t。

      如果获取成功,则返回value值。

      如果map为空,则调用setInitialValue方法返回value。

      可以看出第12行处的方法setInitialValue()只有在线程第一次使用 get() 方法访问变量的时候调用。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

    protected T initialValue() {
        return null;
    }

    该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法,创建匿名内部类重写此方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。

    对于ThreadLocal需要注意的有两点:
    1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
    2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

    ThreadLocal的应用场景

      最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。如:

    /**
     * 数据库连接管理类
     */
    public class ConnectionManager {
     
        /** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */
        private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
        
        public static Connection getCurrConnection() {
            // 获取当前线程内共享的Connection
            Connection conn = threadLocal.get();
            try {
                // 判断连接是否可用
                if(conn == null || conn.isClosed()) {
                    // 创建新的Connection赋值给conn(略)
                    // 保存Connection
                    threadLocal.set(conn);
                }
            } catch (SQLException e) {
                // 异常处理
            }
            return conn;
        }
        
        /**
         * 关闭当前数据库连接
         */
        public static void close() {
            // 获取当前线程内共享的Connection
            Connection conn = threadLocal.get();
            try {
                // 判断是否已经关闭
                if(conn != null && !conn.isClosed()) {
                    // 关闭资源
                    conn.close();
                    // 移除Connection
                    threadLocal.remove();
                    conn = null;
                }
            } catch (SQLException e) {
                // 异常处理
            }
        }
    }

    也可以重写initialValue方法

    private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };
     
    public static Connection getConnection() {
        return connectionHolder.get();
    }

    Hiberante的Session 工具类HibernateUtil

    public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory; //定义SessionFactory
    static {
    try {
    // 通过默认配置文件hibernate.cfg.xml创建SessionFactory
    sessionFactory = new Configuration().configure().buildSessionFactory();
    } catch (Throwable ex) {
    log.error("初始化SessionFactory失败!", ex);
    throw new ExceptionInInitializerError(ex);
    }
    }
    //创建线程局部变量session,用来保存Hibernate的Session
    public static final ThreadLocal session = new ThreadLocal();
    /**
    * 获取当前线程中的Session
    * @return Session
    * @throws HibernateException
    */
    public static Session currentSession() throws HibernateException {
    Session s = (Session) session.get();
    // 如果Session还没有打开,则新开一个Session
    if (s == null) {
    s = sessionFactory.openSession();
    session.set(s); //将新开的Session保存到线程局部变量中
    }
    return s;
    }
    public static void closeSession() throws HibernateException {
    //获取线程局部变量,并强制转换为Session类型
    Session s = (Session) session.get();
    session.set(null);
    if (s != null)
    s.close();
    }
    }

    在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中

    ThreadLocal使用的一般步骤

    1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

    2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

    3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

    ThreadLocal为什么会内存泄漏

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    上面代码中Entry 继承了WeakReference,说明该map的key为一个弱引用,我们知道弱引用有利于GC回收。

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue。但是这些被动的预防措施并不能保证不会内存泄漏:

    • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。

    • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

    为什么使用弱引用

    • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
    • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

      1、可以知道使用弱引用可以多一层保障:理论上弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除;但是如果分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就有可能导致内存泄漏

      2、通常,我们需要保证作为key的ThreadLocal类型能够被全局访问到,同时也必须保证其为单例,因此,在一个类中将其设为static类型便成为了惯用做法,如上面例子中都是用了Static修饰。使用static修饰ThreadLocal对象的引用后,ThreadLocal的生命周期跟Thread一样长,因此ThreadLocalMap的Key也不会被GC回收,弱引用形同虚设,此时就极容易造成ThreadLocalMap内存泄露。

    关键在于threadLocal如果用Static修饰,如果是多线程操作threadlocal,当前线程结束后,ThreadLocal对象作为GCRoot还在其他线程中,这是弱引用就不能被回收,也就是当前Thread中的Map中的key还不会被回收,也就是很多线程中都有threadlocal为key的map不会被回收,那就会出现内存泄露。

    ThreadLocal 最佳实践

    综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

    总结

    • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
    • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
    • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,避免造成内存泄露。
  • 相关阅读:
    定位
    浮动
    标准文档流
    盒模型
    CSS继承性和层叠性
    微信公众平台-信息的获取
    信息系统项目管理师-整体介绍
    Dijkstra算法 c语言实现
    windows下c语言获取程序当前的执行目录,读文件的代码片
    网络流问题,及其代码
  • 原文地址:https://www.cnblogs.com/alimayun/p/13139362.html
Copyright © 2020-2023  润新知