java ThreadLocal 实践与面试要点

Posted BBinChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java ThreadLocal 实践与面试要点相关的知识,希望对你有一定的参考价值。

问题:

1、线程共享进程的内存,如果我们需要线程间各自维护数据避免数据污染。
2、线程变量跨线程池传递

解决方案

定义一个map,key为线程id,value为我们要隔离的变量。
map<threadId, value> threadMap

常见使用场景

通常在系统开发时,初期为了快速开发或者逻辑设计没有很清晰的情况下,一个函数可能包含了多个处理逻辑,为了系统代码结构整洁,符合开闭单一原则,我们需要对函数进行分解,这个时候会发现
多个子函数会用到同一份数据, 比如当一个请求处理时, A函数会远程调用其他服务获取数据,这份数据同样在B函数会用到,那么在拆解的过程中,可以通过参数传递的方式将数据传递给每个需要的子函数。完成第一步重构后,会发现可能传递的参数太多了怎么办,而实际上在一次请求时,这些数据的传递只不过是在一个线程内的状态传递,而这种需要传递的对象,我们通常称之为上下文(Contetxt),这个时候便可以使用 Threalocal来传递数据。

实践

Java标准库提供ThreadLocal类,它可以在一个线程中传递同一个对象。

ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<Data> threadLocalData = new ThreadLocal<>();

它的典型使用方式如下:

void request(data) {
    try {
        threadLocalData.set(data);
        A();
        B();
    } finally {
        threadLocalData.remove();
    }
}

同理我们提出的解决方案,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。

最后,特别注意ThreadLocal一定要在finally中清除:

try {
    threadLocalData.set(data);
    ...
} finally {
    threadLocalData.remove();
}

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

虽然ThreadLocalMap使用了弱引用key,而弱引用的释放发生在垃圾回收,由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏。

扩展

ThreadLocal为什么会内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

ThreadLocal不是线程安全

ThreadLocal设置线程变量的源码


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

其中的getMap方法

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

这个方法在InheritableThreadLocal被重写,所以InheritableThreadLocal可以用来解决不安全的问题,回过头来看其不安全的原因。共用的ThreadLocal虽然按线程存储了ThreadLocalMap用来存放线程的变量,但存放的内容Value是一个引用值,当有其他线程对其进行修改时,当前线程读取的数据是已改后的值。例子如下:

class UnSafeThread implements Runnable {

    public static final ThreadLocal<Number> value = new ThreadLocal<Number>();

    public static Number number = new Number();

    public static int i = 0;

    @Override
    public void run() {
        number.setNum(i++);

        value.set(number);

        try {

            TimeUnit.SECONDS.sleep(2);

            System.out.println(Thread.currentThread()+" 输出 "+number.getNum());

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上文提到的InheritableThreadLocal 可以解决ThreadLocal的局限性,当同时也有其局限


public class TestThreadLocal {

    public static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();

    public static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(1);


    @Test
    public void fun1() {

        THREAD_LOCAL.set(new Person());
        THREAD_POOL.execute(()->{getAndPrintData();});

        Person person = new Person();
        person.setAge(100);

        THREAD_LOCAL.set(person);
        THREAD_POOL.execute(()->{getAndPrintData();});

    }

    private void setData(Person person) {
        System.out.println("set 数据,线程名:"+Thread.currentThread().getName());
        THREAD_LOCAL.set(person);
    }

    private Person getAndPrintData() {
        Person person = THREAD_LOCAL.get();
        System.out.println("get 数据,线程名:"+Thread.currentThread().getName()+" 数据为: "+person.toString());
        return person;
    }
}

当线程池数目为1时,两次执行会复用同一个线程,输出内容如下:

get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)

会发现Person的age同为18,这是因为复用线程内的ThreadLocalMap为同一个,所以不会绑定新的数据

当线程池数目为2时,即使线程池有空闲线程,而为达到CoreSize时,依然会初始化新线程,两次执行结果如下:

get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-2,数据为:TestThreadLocal.Person(age=100)

线程的Init初始化过程会重新绑定值,所以第二线程输出的age为100.

InheritableThreadLocal 在Init时的源码

在Thread Init函数中有核心的处理部分,InheritableThreadLocals会将父类的ThreadLocalMap传递给子类

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

虽然实现了父子线程的变量传递,但仍局限于非父子线程间的变量传递,比方说 调用链跟踪的使用场景(也有其他方式解决)。
针对跨线程池,阿里提出了TTL的组件。

TransmittableThreadLocal

TTL是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。

TransmittableThreadLocal简称TTL,InheritableThreadLocal简称ITL

它的官网是:TTL

以上是关于java ThreadLocal 实践与面试要点的主要内容,如果未能解决你的问题,请参考以下文章

JAVA面试经历汇总

面试题2020-03-31说说你对ThreadLocal的理解

粗略整理的java面试题

java面试要点

Java面试题必备知识之ThreadLocal

Java面试必问,ThreadLocal终极篇