理解ThreadLocal

作者: bing__chen | 来源:发表于2017-03-29 21:58 被阅读0次

    概述

    ThreadLocal是一种线程封闭技术,用于隔离线程间的数据,从而避免使用同步控制。

    一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭。

    ThreadLocal为每条使用它的线程提供专属的内部变量。在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相互独立,互不影响。

    基本用法

    ThreadLocal对象通常被设计为类的私有静态类型(private static)字段,用来关联线程的某种状态。

    举个例子:

    public class ThreadLocalTest {
    
        private static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
            protected Integer initialValue() {
                return new Integer(0);
            }
        };
    
        public static class MyRunnable implements Runnable {
    
            private void save() {
                System.out.printf("线程[%s]保存数据, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());
            }
    
            public void run() {
                while(true) {
                    counter.set( (counter.get() + 1) % 10 );
    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    System.out.printf("线程[%s]处理业务, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());
    
                    if(counter.get() == 0) {
                        save();
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            MyRunnable runnable = new MyRunnable();
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
        }
    
    }
    

    这个例子很简单,业务线程内有一个循环在不断的处理业务,假设每次处理业务会产生一些数据,出于性能考虑,希望每处理完10次业务才批量保存数据。

    ThreadLocal主要有四个方法:

    • initialValue
    • get
    • set
    • remove(例子中未使用)

    下面逐一简介。

    initialValue方法

    initialValue是设计给子类重写的方法,用以返回初始化的线程内部变量。在线程第一次调用get时它会被调用,但如果在调用get之前已经调用了set为线程内部变量设过值,则该方法不会被调用。所以,如果你希望手动调用set来初始化线程内部变量,则不必重写initialValue

    通常initialValue只会被调用一次,除非手动调用remove清除了内部变量,之后又调用get方法,这时initialValue会再被调用初始化一个新的内部变量返回。

    get方法

    get用以获取ThreadLocal对象关联的线程内部变量。

    public T get()
    

    set方法

    set用以设置ThreadLocal对象关联的线程内部变量的值。

    public void set(T value)
    

    remove方法

    remove用以移除ThreadLocal对象关联的线程内部变量,某些情况需要用它来显式地移除,以防止内存泄漏。

    public void remove()
    

    你可能会问,为什么要这么复杂,在run里面使用一个方法局部变量来做计数器岂不是更简单。

    对于这个例子来说,确实如此,ThreadLocal的功能性和方法局部变量没有本质的区别。

    不过,ThreadLocal相较于方法局部变量,可以帮你管理线程内部变量,降低了同一线程内多个方法和组件间传递参数的复杂度。

    内部实现

    Paste_Image.png

    如上图所示,ThreadLocal机制主要由EntryThreadLocalMapThreadThreadLocal这四个类相互协作实现的。

    下面分析这四个类各自的职责和协作

    Entry

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

    Entry的定义很简单,它扩展自ThreadLocal类型的WeakReference类,是一个key-value对类。key是ThreadLocal对象的弱引用,value是线程的内部变量。

    Entry使用弱引用作为key目的是,希望在外部不再需要访问ThreadLocal对象时可以让GC尽快地回收对象,而不必等到线程结束后。

    当GC回收ThreadLocal对象后,再通过Entry.get()获取ThreadLocal对象时返回null,这使得内部能够感知什么时候不需要再持有对value的引用,从而释放Entry对象的引用,进而释放value的引用,这时如果value在外部没有任何引用的话(通常你不应该在外部持有对value的引用),随后被GC回收。这种感知和释放的行为发生在ThreadLocalgetsetremove操作时。

    Thread

    Thread内部持有一个ThreadLocalMap类型引用的成员变量。

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

    threadLocals的初始值为null,它会延迟到初次访问时才实例化,即线程首个ThreadLocal对象调用get方法时才为threadLocals创建对象。

    ThreadLocalMap

    ThreadLocalMap是为ThreadLocal而设计的hash map,内部维护着一个哈希table数组,table内保存Entry的对象,通过ThreadLocal的哈希码可索引到(哈希码需转成数组下标)。

    ThreadLocal

    ThreadLocal是整个机制的总导演,对外,它提供使用的接口;对内,它协调类之间的相互协作。

    ThreadLocal内部不会持有对线程内部变量的引用,线程内部变量的引用由Entry对象持有,而Entry对象寄存在ThreadLocalMap内的table中。

    每一个ThreadLocal对象对应一个唯一的哈希码(threadLocalHashCode),通过这个哈希码可以从ThreadLocalMap中索引出对应的Entry,从而获得线程内部变量。

    这里很巧妙,ThreadLocal对象与线程内部变量之间通过Entry对象间接关联,在内部只有Entry对象持有对ThreadLocal对象的弱引用,这样当外部不再使用ThreadLocal对象后,GC能够回收ThreadLocal对象,当内部探测到ThreadLocal对象被回收后就接着释放Entry对象。

    最后

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

    通常在Java的世界里,我们不需要关系对象的释放,大部分情况下GC会自动帮我们回收。

    但是如果使用ThreadLocal不当,是有可能导致内存泄漏的。

    ThreadLocal释放内部变量通常在以下时机:

    • 线程结束后
    • 显式调用remove
    • 在调用getset时,如果探测到ThreadLocal对象的弱引用对象get返回null顺便释放。

    所以,如果线程存活的生命周期很长,特别是和进程一样长的话,就要特别注意防止ThreadLocal引入内存泄漏的风险,在不需要再使用某个线程内部变量时记得显式调用remove清理掉。

    相关文章

      网友评论

        本文标题:理解ThreadLocal

        本文链接:https://www.haomeiwen.com/subject/uwiyottx.html