ThreadLocal的原理与使用

ThreadLocal的原理与使用

一、ThreadLocal是什么

        ThreadLocal,它不是一个线程,而是线程的一个本地化对象。ThreadLocal是为了解决多个线程同时访问一个变量时的并发问题,在多线程环境下,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。这个变量里面的值是和其他线程分割开来的,变量的值只有当前线程能访问到。保证了多线程环境下当前线程中变量的安全性。

 

二、实现原理

        ThreadLocal为每一个线程维护一个变量副本,在ThreadLocal类中有一个静态内部类ThreadLocalMap,用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

1.ThreadLocal的接口方法

ThreadLocal类提供的四个接口方法:

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

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

(3) public void remove()用来删除当前线程中变量的副本,该方法是JDK 5.0新增的方法,目的是为了减少内存的占用,防止内存泄漏。虽然当线程结束后,线程的变量副本会被自动垃圾回收,但是在线程结束前会有造成内存泄漏的情况,可以调用该方法来避免,后面会详细介绍。 

(4) protected Object initialValue()返回该线程局部变量的初始值,该方法一般在子类中重写用来初始化变量副本。如果不在子类中重写,这个方法会延迟调用,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,在ThreadLocal中的实现直接返回null。

下面来先看一下get()方法的源码:

    /**
     * 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);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();   //若当前线程还未创建ThreadLocalMap,
                                             // 则调用此方法创建并返回初始值。
    }

接着看getMap()方法中做了什么:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;   //返回的是当前线程t中的一个成员变量threadLocals
    }

接着去Thread类中看一下threadLocals是什么东西:

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

其实就是一个ThreadLocal的内部类ThreadLocalMap对象,接着看一下ThreadLocalMap是如何实现的:

static class ThreadLocalMap {
  //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量为16
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 存储线程的中所有ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
     */
    private Entry[] table;

接下来看一下是如何创建ThreadLocalMap的,也就是get()方法中最后一句调用的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)
            map.set(this, value);
        else
            createMap(t, value);  //创建ThreadLocalMap
        return value;
    }
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

至此,可以看到为每个线程保存变量副本的方法也就是为每个线程new一个ThreadLocalMap对象threadLocals,以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后使用接口方法分别对副本变量进行操作。

 

三、使用场景

1.数据库连接管理

同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

以下代码摘自:https://blog.csdn.net/sean417/article/details/69948561

    import java.sql.Connection;    
    import java.sql.DriverManager;    
    import java.sql.SQLException;    
        
    public class ConnectionManager {    
        
        private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {    
            @Override    
            protected Connection initialValue() {    
                Connection conn = null;    
                try {    
                    conn = DriverManager.getConnection(    
                            "jdbc:mysql://localhost:3306/test", "username",    
                            "password");    
                } catch (SQLException e) {    
                    e.printStackTrace();    
                }    
                return conn;    
            }    
        };    
        
        public static Connection getConnection() {    
            return connectionHolder.get();    
        }    
        
        public static void setConnection(Connection conn) {    
            connectionHolder.set(conn);    
        }    
    }    

2.ThreadLocal实例

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
        protected Long initialValue() {
            return Thread.currentThread().getId();
        };
    };
    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
        protected String initialValue() {
            return Thread.currentThread().getName();
        };
    };
 
     
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
 
        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());
     
         
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

四、内存泄露问题

1.threadlocal实例被回收

在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收。 这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。只有当前thread结束以后, current thread就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收。 所以只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。

为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

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

为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  1. key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

  2. key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:

由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

2.线程对象未回收

线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程池中对线程管理都是采用线程复用的方法。在线程池中线程非常难结束甚至于永远不会结束。就可能出现内存泄露。

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

上文提到remove()方法可以避免内存泄漏,是因为在调用get()/set()/remove()时遍历Map,会自动清理key为null的value。

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

使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

 

五、与同步机制的比较

相同点:

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题

不同点:

同步机制中通过对象的锁机制保证同一时间只有一个线程来访问变量,这是该变量是多个线程共享的,使用同步机制要缜密的分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放掉锁等复杂的问题,程序设计和编写难度大。

ThreadLocal则为每一个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每一个线程都有自己的变量副本,从而也就没有必要进行同步了。ThreadLocal提供了线程安全的对象的封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

总的来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间的”方式——访问串行化,对象共享化。而ThreadLocal则采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLocal和线程同步机制是多线程访问的两种不同的方式,分别在不同的应用场景中发挥作用。

 

       欢迎  扫一扫  下方二维码

   关注我的公众号 阅读更多文章

       

参考文档:

https://blog.csdn.net/lhqj1992/article/details/52451136

https://blog.csdn.net/see__you__again/article/details/51244946

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值