战狂粗人张

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

一.ThreandLocal是什么

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。早在JDK 1.2的版本中就提供java.lang.ThreadLocal。

ThreadLocal是一个关于创建线程局部变量的类。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

必须要区分ThreadLocal和Syncronized这种同步机制,两者面向的问题领域是不一样的。sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。

而ThreadLocal是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,

更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;

而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

 

二.ThreandLocal能做什么

1、避免一些参数传递

进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

每当我访问一个页面的时候,浏览器都会从硬盘中找到对应的Cookie发送过去。

浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去。

浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),

不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的….

那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写<input name= cookie/>来进行传递参数。

2、线程间数据隔离

 

3、进行事务操作,用于存储线程事务信息。

 

4、数据库连接,Session会话管理。

在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

那么,数据库连接池的连接怎么管理呢??由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

 

三.ThreandLocal原理

ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。

我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。

虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。

按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。

但Spring的DAO模板类并未采用线程同步机制,因为线程同步限制了并发访问,会带来很大的性能损失。

此外,通过代码同步解决性能安全问题挑战性很大,可能会增强好几倍的实现难度。

那模板类究竟仰丈何种魔法神功,可以在无需同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!

ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。

要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。

 

1、源代码分析

 /**
     * 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
     * <tt>initialValue</tt> 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<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的类似
}

在JDK1.5以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。

ThreadLocal<T>类提供了4个可用的方法(基于JDK1.7版本):

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

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

(3)public void remove()将当前线程局部变量的值删除。

该方法是JDK 5.0新增的方法,目的是为了减少内存的占用。

需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

(4)protected T initialValue()返回该线程局部变量的初始值。

该方法是一个protected的方法,显然是为了让子类覆盖而设计的。

这个方法是一个延迟调用方法,在线程第1次调用get()或set(T value)时才执行,并且仅执行1次,ThreadLocal中的缺省实现是直接返回一个null。

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

 

总结:

1、每个Thread维护着一个ThreadLocalMap的引用;

2、ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储;

3、调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象;

4、调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

5、ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。

 

四.ThreandLocal使用

启动两个线程,第一个线程中存储的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();
    }
}
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());
    }
}
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

 

四.ThreandLocal问题

1、ThreadLocal的内存泄露问题

首先要理解内存泄露(memory leak)和内存溢出(out of memory)的区别。

内存溢出是因为在内存中创建了大量在引用的对象,导致后续再申请内存时没有足够的内存空间供其使用。

内存泄露是指程序申请完内存后,无法释放已申请的内存空间,(不再使用的对象或者变量仍占内存空间)。

根据上面的源码,我们知道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。

 

 

posted on 2020-05-13 18:05  战狂粗人张  阅读(221)  评论(0编辑  收藏  举报