• 关于ThreandLocal


    一.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。

     

     

  • 相关阅读:
    Use 'mysqld --thread_stack=#' to specify a bigger stack.
    Caused by: com.mysql.jdbc.exceptions.MySQLSyntaxErrorException: You have an error in your SQL syntax;
    interface interface com.alibaba.dubbo.common.compiler.Compiler, class line com.alibaba.dubbo.common.compiler.support.JavassistCompiler) in
    javax.servlet.http.HttpServlet was not found on the Java Build Path
    js_jquery单机事件不起作用
    JavaScript 执行环境及作用域
    闭包的使用场景
    YUI css reset
    比较经典的数组去重和数组排序
    css 响应式布局
  • 原文地址:https://www.cnblogs.com/ZJOE80/p/12884016.html
Copyright © 2020-2023  润新知