深入剖析ThreadLocal实现原理

来源:互联网 发布:centos解压rar 编辑:程序博客网 时间:2024/05/18 03:25

这里写图片描述

在2017京东校园招聘笔试题中遇到了描述ThreadLocal的实现原理和内存泄漏的问题,之前看过ThreadLocal的实现原理,但是网上有很多文章将的很乱,其中有很多文章将ThreadLocal与线程同步机制混为一谈,特别注意的是ThreadLocal与线程同步无关,并不是为了解决多线程共享变量问题!
ThreadLocal官网解释:

这里写图片描述

翻译过来的大概意思就是:ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过getset方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。

总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量 副本,提供了保持对象的方法和避免参数传递的复杂性。

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

这里写图片描述

ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:

  1. void set(Object value)设置当前线程的线程局部变量的值。
  2. public Object get()该方法返回当前线程所对应的线程局部变量。
  3. public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  4. protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢?

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

源代码:

/** Returns the value in the current thread's copy of this thread-local variable.  If the variable has no value for thecurrent 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);//获取对应ThreadLocal的变量值        if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }    return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。}//设置变量的值public void set(T value) {   Thread t = Thread.currentThread();   ThreadLocalMap map = getMap(t);   if (map != null)       map.set(this, value);   else       createMap(t, 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);   return value;}/**为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中@param t the current thread@param firstValue value for the initial entry of the map*/void createMap(Thread t, T firstValue) {    t.threadLocals = new ThreadLocalMap(this, firstValue);}//删除当前线程中ThreadLocalMap对应的ThreadLocalpublic void remove() {       ThreadLocalMap m = getMap(Thread.currentThread());       if (m != null)           m.remove(this);}

上述是在ThreadLocal类中的几个主要的方法,他们的核心都是对其内部类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,以为对其扩充也必须是2的指数      */    private static final int INITIAL_CAPACITY = 16;    /**     * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。     */    private Entry[] table;    ///....其他的方法和操作都和map的类似}

总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。

这里写图片描述

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

问:那么是不是说ThreadLocal的实例以及其值存放在栈上呢?

其实不是,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而ThreadLocal的值其实也是被线程实例持有。

它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

关于堆和栈的比较,请参考Java中的堆和栈的区别

这里写图片描述

既然上面提到了ThreadLocal只对当前线程可见,是不是说ThreadLocal的值只能被一个线程访问呢?

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。

如下,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

private void testInheritableThreadLocal() {    final ThreadLocal threadLocal = new InheritableThreadLocal();    threadLocal.set("droidyue.com");    Thread t = new Thread() {        @Override        public void run() {            super.run();            Log.i(LOGTAG, "testInheritableThreadLocal =" + threadLocal.get());        }    };    t.start();}

上面的代码输出的日志信息为

I/MainActivity( 5046): testInheritableThreadLocal =droidyue.com

使用InheritableThreadLocal可以将某个线程的ThreadLocal值在其子线程创建时传递过去。因为在线程创建过程中,有相关的处理逻辑。

//Thread.java private void init(ThreadGroup g, Runnable target, String name,                      long stackSize, AccessControlContext acc) {        //code goes here        if (parent.inheritableThreadLocals != null)            this.inheritableThreadLocals =                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);        /* Stash the specified stack size in case the VM cares */        this.stackSize = stackSize;        /* Set thread ID */        tid = nextThreadID();}

上面代码就是在线程创建的时候,复制父线程的inheritableThreadLocals的数据。

这里写图片描述

实现单个线程单例以及单个线程上下文信息存储,比如交易id等

实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例

承载一些线程相关的数据,避免在方法中来回传递参数

这里写图片描述

  1. Java ThreadLocal
  2. Threadlocals and memory leaks in J2EE
  3. Java Thread Local – How to use and code sample
  4. ThreadLocal in Java – Example Program and Tutorial
1 0
原创粉丝点击