深入理解ThreadLocal

    xiaoxiao2025-01-14  9

    ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。线程局部变量(ThreadLocal)的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突

    从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

    通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

    ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

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

    ThreadLocal 实现原理

    ThreadLocal

    ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

    也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

    值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

    API说明

    ThreadLocal()

              创建一个线程本地变量。

    T get()

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

    protected  T initialValue()

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

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

    void remove()

              移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。

    void set(T value)

              将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。

    在程序中一般都重写initialValue方法,以给定一个特定的初始值。

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    应用实例:

    1.

    package com.liuhui.thread; public class ThreadLocalDemo { private static ThreadLocal<Integer>t=new ThreadLocal<Integer>(); public static int test(){ Integer num=t.get(); if(num==null){ num=10; t.set(num); } return num; } public static void main(String[] args) { System.out.println(test()); } }

    2.

    package com.liuhui.thread; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class TestConn { private static ThreadLocal<Connection>t=new ThreadLocal<Connection>(); public static Connection test() throws SQLException { Connection conn=t.get(); if(conn==null){ conn=DriverManager.getConnection("","",""); t.set(conn); } return conn; } public static void main(String[] args) throws Exception { System.out.println(test()); } }

     

    3.

    package com.func.axc.threadlocal;      import java.util.concurrent.ExecutorService;   import java.util.concurrent.Executors;      public class ThreadLocalTest {              //创建一个Integer型的线程本地变量        static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {           @Override           protected Integer initialValue() {               return 0;           }       };              static class Task implements Runnable{           private int num;                      public Task(int num) {               this.num = num;           }              @Override           public void run() {               //获取当前线程的本地变量,然后累加10次               Integer i = local.get();               while(++i<10);               System.out.println("Task " + num + "local num resutl is " + i);           }       }              static void Test1(){           System.out.println("main thread begin");           ExecutorService executors = Executors.newCachedThreadPool();           for(int i =1;i<=5;i++) {               executors.execute(new Task(i));           }           executors.shutdown();           System.out.println("main thread end");       }              public static void main(String[] args){           Test1();       }       }  

    输出结果:

    可以看到各个线程之间的变量是独门的,不会相影响。

     

    通过上面的一个实例,简单的了解了ThreadLocal的用法,下面再来看下其源码实现,首先看一下ThreadLocal的图解:

     

    图解ThreadLocal

    每个线程可能有多个ThreadLocal,同一线程的各个ThreadLocal存放于同一个ThreadLocalMap中。

    图解ThreadLocal(JDK8).vsdx原图下载地址:https://github.com/zxiaofan/JDK-Study/tree/master/src/java1/lang/threadLocal

    ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

    下面分析其源码

    首先是其包含的方法:

    它的构造函数不做什么:

    public ThreadLocal() {   }  

     

    其实主要的也就下面几个方法:

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

    下面正式分析源码

    分析源码前,先看一下Thread类中有一行:

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

    其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:

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

    从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,并不是网上大多数的例子key是线程的名字或者标识。

    (1)get

    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();   }  

    这个上方法就是用来取得变量的副本的,注意到它先取得了当前线程对象,接下来使用了getMap返回一个ThreadLocalMap

    读取实例时,线程首先通过getMap(t)方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射

    private Entry getEntry(ThreadLocal<?> key){ int i= key.threadLocalHashCode&(table.length-1); Entry e= table[i]; if(e!=null&& e.get()== key) return e; else return getEntryAfterMiss(key, i, e);

    获取到 ThreadLocalMap 后,通过map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象。

    如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。

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

    然后可以知道ThreadLocalMap这个竟然是从线程中取到的,好,再打开线程类看看

    发现Thread类中有这样一个变量:

    ThreadLocal.ThreadLocalMap threadLocals = null;  

    也变是说每一个线程都有自己一个ThreadLocalMap。

    在我们第一次调用get()函数 时,getMap函数返回的是一个null的map.接着就调用setInitialValue()

    看看setInitialValue,它才是真正去初始化map的地方!

    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);       return value;   }  

    其中initialValue这个方法就是我们要重写的,一般我们在这里通过一个new 方法返回一个新的变量实例

    protected T initialValue() {       return null;   } 

     

    因为是第一次调用get(),所以getMap后的map还是为null。这时就调用到createMap

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

    终于创建ThreadLocalMap!

    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);   }  

     

    这里就将Thread和我们的ThreadLocal通过一个map关联起来。意思是每个Thread中都有一个ThreadLocal.ThreadLocalMap。其中Key为ThreadLocal这个实例,value为每次initialValue()得到的变量!

    接下来如果我们第二次调用get()函数,这里就会进入if方法中去!

     

    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();   }  

    进入If方法中后。就会根据当前的thradLocal实例为Key,取得thread中对应map的vale.其中getEntry方法只是取得我们的key-value对。注意,这时的table其实就是在ThreadLocal实例中都会记录着每个和它关联的Thread类中的ThreadLocalMap变量

    private Entry getEntry(ThreadLocal<?> key) {       int i = key.threadLocalHashCode & (table.length - 1);       Entry e = table[i];       if (e != null && e.get() == key)           return e;       else           return getEntryAfterMiss(key, i, e);   }  

    是取得我们的key-value对之后就可取value了,然后就是返回result.如果这时取不到entry,那么又会调用到setInitalValue()方法,过程又和上面的一样了。这里就不说了!

    (2)set

    这个方法就是重新设置每一个线程的本地ThreadLocal变量的值

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

    这里取得当前线程,然后根据当前的thradLocal实例取得其map。然后重新设置 map.set(this, value);这时这个线程的thradLocal里的变量副本就被重新设置值了!

    (3)remove

    就是清空ThreadLocalMap里的value,这样一来。下次再调用get时又会调用 到initialValue这个方法返回设置的初始值

    public void remove() {       ThreadLocalMap m = getMap(Thread.currentThread());       if (m != null)           m.remove(this);   }  

    到这里,我们就可以理解ThreadLocal究竟是如何工作的了

    Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的

    总结:

    1、每个线程都有自己的局部变量 每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释) 2、独立于变量的初始化副本 ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。 3、状态与某一个线程相关联 ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

     

    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永远无法回收,造成内存泄漏。

    其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

    但是这些被动的预防措施并不能保证不会内存泄漏:

    使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

    为什么使用弱引用

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

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

    To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

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

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

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

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

    ThreadLocal 最佳实践

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

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

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

     

    ThreadLocal 使用场景:

     

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

      如:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    private static ThreadLocal<Connection> connectionHolder

    = new ThreadLocal<Connection>() {

    public Connection initialValue() {

        return DriverManager.getConnection(DB_URL);

    }

    };

     

    public static Connection getConnection() {

    return connectionHolder.get();

    }

       下面这段代码摘自:

      http://www.iteye.com/topic/103804

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    private static final ThreadLocal threadSession = new ThreadLocal();

     

    public static Session getSession() throws InfrastructureException {

        Session s = (Session) threadSession.get();

        try {

            if (s == null) {

                s = getSessionFactory().openSession();

                threadSession.set(s);

            }

        } catch (HibernateException ex) {

            throw new InfrastructureException(ex);

        }

        return s;

    }

     

    参考:http://blog.csdn.net/evankaka/article/details/51705661

    转载请注明原文地址: https://ju.6miu.com/read-1295472.html
    最新回复(0)