资讯专栏INFORMATION COLUMN

ThreadLocal

cppprimer / 2845人阅读

摘要:将当前线程局部变量的值删除。是调用当期线程,返回当前线程中的一个成员变量。的一个使用示例测试类启动两个线程,第一个线程中存储的为,第二个线程中存储的为。

ThreadLocal是什么?

ThreadLocal 源码解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

大概的理解是:ThreadLocal类用来提供线程内部的本地变量。这些变量在每个线程内会有一个独立的初始化的副本,和普通的副本不同,每个线程只能访问自己的副本(通过get或set方法访问)。在一个类里边ThreadLocal成员变量通常由private static修饰。

简单地说,ThreadLocal的作用就是为每一个线程提供了一个独立的变量副本,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

我们必须要区分ThreadLocal和Syncronized这种同步机制,两者面向的问题领域是不一样的。sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。而ThreadLocal从本质上讲,无非是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

ThreadLocal中的方法

在JDK1.5以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal
ThreadLocal类提供了4个可用的方法(基于JDK1.7版本):

void set(T value)设置当前线程的线程本地变量的值。

public T get()该方法返回当前线程所对应的线程局部变量。

public void remove()将当前线程局部变量的值删除。
该方法是JDK 5.0新增的方法,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected T initialValue()返回该线程局部变量的初始值。
该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
这个方法是一个延迟调用方法,在线程第1次调用get()或set(T value)时才执行,并且仅执行1次,ThreadLocal中的缺省实现是直接返回一个null。

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量。

注意,在JDK1.8版本中还多了如下的这个方法:

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param  the type of the thread local"s value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static  ThreadLocal withInitial(Supplier supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
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 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);    //获取对应ThreadLocal的变量值
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();    //若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
    }

    // 是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // java.lang.Thread类下, 实际上就是一个ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;


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


    /**
     * Sets the current thread"s copy of this thread-local variable
     * to 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);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, 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);
    }


    /**
     * 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
     * initialValue method in the current thread.
     *
     * @since 1.5
     */
     public 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> {
           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的类似
}
ThreadLocal的几个问题 为什么不直接用线程id来作为ThreadLocalMap的key?

这个问题很容易解释,因为一个线程中可以有多个ThreadLocal对象,所以ThreadLocalMap中可以有多个键值对,存储多个value值,而如果使用线程id作为key,那就只有一个键值对了。

ThreadLocal的内存泄露问题

首先要理解内存泄露(memory leak)和内存溢出(out of memory)的区别。内存溢出是因为在内存中创建了大量在引用的对象,导致后续再申请内存时没有足够的内存空间供其使用。内存泄露是指程序申请完内存后,无法释放已申请的内存空间,(不再使用的对象或者变量仍占内存空间)。

根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

如上图,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结束以后, Thread Ref就不会存在栈中,强引用断开, Thread, ThreadLocalMap, Entry将全部被GC回收。但如果是线程对象不被回收的情况,比如使用线程池,线程结束是不会销毁的,就可能出现真正意义上的内存泄露。

ThreadLocalMap设计时的对上面问题的对策:
当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

但无论如何,我们应该考虑到何时调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。

ThreadLocal的一个使用示例 测试类:ThreadLocalTest.java

启动两个线程,第一个线程中存储的userid为1,第二个线程中存储的userid为2。

package com.lzumetal.multithread.threadlocal;

import java.math.BigDecimal;

public class ThreadLocalTest {

    public static void main(String[] args) throws InterruptedException {
        Order order01 = new Order(1, 1, new BigDecimal(10), 1);
        new Thread(new OrderHandler(1, order01)).start();

        Order order02 = new Order(2, 2, new BigDecimal(20), 2);
        new Thread(new OrderHandler(2, order02)).start();
    }
}
OrderHandler.java
package com.lzumetal.multithread.threadlocal;

public class OrderHandler implements Runnable {

    private static OrderService orderService = new OrderService();

    private Integer userId;
    private Order order;

    public OrderHandler(Integer userId, Order order) {
        this.userId = userId;
        this.order = order;
    }

    @Override
    public void run() {
        EnvUtil.getUserIdContext().set(userId);
        orderService.addOrder(order);
        orderService.updateStock(order.getGoodId(), order.getGoodCount());
    }
}
OrderService.java
package com.lzumetal.multithread.threadlocal;

public class OrderService {


    /**
     * 新增订单
     *
     * @param order
     */
    public void addOrder(Order order) {
        Integer userId = EnvUtil.getUserIdContext().get();
        System.out.println(Thread.currentThread().getName() + "新增订单服务中获取用户id-->" + userId);
    }


    /**
     * 更新库存
     *
     * @param goodId
     * @param goodCount
     */
    public void updateStock(Integer goodId, Integer goodCount) {
        //虽然更新库存不需要关注userId,但是在这里也一样能够获取到
        Integer userId = EnvUtil.getUserIdContext().get();
        System.out.println(Thread.currentThread().getName() + "在更新库存中获取用户id-->" + userId);
    }


}
运行结果
Thread-0新增订单服务中获取用户id-->1
Thread-1新增订单服务中获取用户id-->2
Thread-0在更新库存中获取用户id-->1
Thread-1在更新库存中获取用户id-->2

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76437.html

相关文章

  • Java面试题必备知识之ThreadLocal

    摘要:方法,删除当前线程绑定的这个副本数字,这个值是的值,普通的是使用链表来处理冲突的,但是是使用线性探测法来处理冲突的,就是每次增加的步长,根据参考资料所说,选择这个数字是为了让冲突概率最小。 showImg(https://segmentfault.com/img/remote/1460000019828633); 老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学...

    Maxiye 评论0 收藏0
  • ThreadLocal详解

    摘要:在方法中取出开始时间,并计算耗时。是一个数组主要用来保存具体的数据,是的大小,而这表示当中元素数量超过该值时,就会扩容。如果这个刚好就是当前对象,则直接修改该位置上对象的。 想要获取更多文章可以访问我的博客 - 代码无止境。 什么是ThreadLocal ThreadLocal在《Java核心技术 卷一》中被称作线程局部变量(PS:关注公众号itweknow,回复Java核心技术获取该...

    2501207950 评论0 收藏0
  • 深入了解Threadlocal

    摘要:通过将保存在中,每个线程都会拥有属于自己的,代码如下所示然后你就可以安心地调用了,不用考虑线程安全问题。这样设计的好处就是,当线程死掉之后,没有强引用,方便收集器回收。 前言 想必大家都对Threadlocal很熟悉吧,今天我们就一起来深入学习一下。Threadlocal我更倾向于将其翻译成线程局部变量。它有什么用处呢?Threadlocal对象通常用于防止对可变的单实例变量或全局变量...

    qiangdada 评论0 收藏0
  • 您有一份ThreadLocal完全解析手册

    摘要:返回索引位置的值。因为依赖于静态成员变量的关系,所以它的肯定唯一获取当前线程。位置还没有初始化第一次这个,直接将放到的位置。在线程池模式下,生命周期伴随着线程一直存在,可能出现内存泄漏的情况,最好手动调用方法。 本文原创地址,:jsbintask的博客(食用效果最佳),转载请注明出处! 前言 ThreadLocal是jdk中一个非常重要的工具,它可以控制堆内存中的对象只能被指定线程访问,如...

    刘东 评论0 收藏0
  • ThreadLocal 内部实现和应用场景(慎用,可能有内存泄露)

    摘要:而应用场景更多是想共享一个变量,但是该变量又不是线程安全的,那么可以用维护一个线程一个实例。因为创建这个对象本身很费时的,而且我们也知道本身不是线程安全的,也不能缓存一个共享的实例,为此我们想到使用来给每个线程缓存一个实例,提高性能。 很多人都知道java中有ThreadLocal这个类,但是知道ThreadLocal这个类具体有什么作用,然后适用什么样的业务场景还是很少的。今天我就尝...

    heartFollower 评论0 收藏0
  • Java 多线程(7): ThreadLocal 的应用及原理

    摘要:但是还有另外的功能看的后一半代码作用就是扫描位置之后的数组直到某一个为的位置,清除每个为的,所以使用可以降低内存泄漏的概率。 在涉及到多线程需要共享变量的时候,一般有两种方法:其一就是使用互斥锁,使得在每个时刻只能有一个线程访问该变量,好处就是便于编码(直接使用 synchronized 关键字进行同步访问),缺点在于这增加了线程间的竞争,降低了效率;其二就是使用本文要讲的 Threa...

    shadajin 评论0 收藏0

发表评论

0条评论

cppprimer

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<