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

原理

TransmittableThreadLocal是在线程池提交任务之前将父线程的数据拷贝一份,然后在线程执行前,将这份拷贝数据复制到该线程的inheritableThreadLocals中,这里的拷贝是浅拷贝。
具体流程图如下:
在这里插入图片描述

具体代码如下:

public class TransmittableThreadLocalDemo {
    private static TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    	// 当前线程设置数据
        transmittableThreadLocal.set("全都能看见");

        // 创建线程池
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("ProcessMessage-pool-%d").build();
        ExecutorService executorService = new ThreadPoolExecutor(1, 1, 2000,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(200),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);

        Future<?> submit = ttlExecutorService.submit(() -> {
            // 线程池中新创建的线程是可以显示数据
            System.out.println(transmittableThreadLocal.get());

            // 修改线程池里子线程变量
            transmittableThreadLocal.set(null);
        });
        Object o = submit.get();

		// 设置当前线程变量
        transmittableThreadLocal.set("变一下吧");
        ttlExecutorService.submit(() -> {
            // 复用的线程获取数据,获取到上层线程设置的变量
            System.out.println(transmittableThreadLocal.get());
        });
        Object o1 = submit.get();

        ttlExecutorService.shutdown();

    }
}

其他问题
数据拷贝问题
InheritableThreadLocal、TransmittableThreadLocal在拷贝父线程数据时,默认都是采用浅拷贝的方式。比如说:

public class InheritableThreadLocalDefectDataDemo {
    private static InheritableThreadLocal<UserInfo> infoInheritableDataThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        UserInfo userInfo = UserInfo.builder()
                .normalMessage("normalMessage")
                .userDetailInfo(UserDetailInfo.builder().innerMessage("innerMessage").build())
                .build();
        infoInheritableDataThreadLocal.set(userInfo);

        // 创建线程池
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("ProcessMessage-pool-%d").build();
        ExecutorService executorService = new ThreadPoolExecutor(1, 1, 2000,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(200),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());

        Future<?> submit = executorService.submit(() -> {

            System.out.println(infoInheritableDataThreadLocal.get().getNormalMessage());
            System.out.println(infoInheritableDataThreadLocal.get().getUserDetailInfo().getInnerMessage());
            // 其他线程修改数据会影响到其他线程
            infoInheritableDataThreadLocal.get().setUserDetailInfo(null);
        });
        Object o = submit.get();

        // 父线程拿不到数据,被线程池里的子线程所修改
        System.out.println(infoInheritableDataThreadLocal.get().getUserDetailInfo());

        executorService.shutdown();
    }
}

如果你希望采用深拷贝来拷贝数据,就得自己写一个类继承InheritableThreadLocal并重写childValue方法。例如:

public class InheritableThreadLocalDefectDataDemo {
    private static InheritableThreadLocal<UserInfo> infoInheritableDataThreadLocal = new InheritableThreadLocal<>();
    //自定义InheritableThreadLocal
    private static MyInheritableThreadLocal<UserInfo> myInheritableThreadLocal = new MyInheritableThreadLocal<>();


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        UserInfo userInfo = UserInfo.builder()
                .normalMessage("normalMessage")
                .userDetailInfo(UserDetailInfo.builder().innerMessage("innerMessage").build())
                .build();
        UserInfo userInfo2 = UserInfo.builder()
                .normalMessage("normalMessage")
                .userDetailInfo(UserDetailInfo.builder().innerMessage("innerMessage").build())
                .build();
        infoInheritableDataThreadLocal.set(userInfo2);
        myInheritableThreadLocal.set(userInfo);

        // 创建线程池
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("ProcessMessage-pool-%d").build();
        ExecutorService executorService = new ThreadPoolExecutor(1, 1, 2000,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(200),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());

        Future<?> submit = executorService.submit(() -> {

            System.out.println(infoInheritableDataThreadLocal.get().getNormalMessage());
            System.out.println(infoInheritableDataThreadLocal.get().getUserDetailInfo().getInnerMessage());
            System.out.println(myInheritableThreadLocal.get().getUserDetailInfo().getInnerMessage());
            // 其他线程修改数据会影响到其他线程
            infoInheritableDataThreadLocal.get().setUserDetailInfo(null);
			// 当前子线程数据更改
            myInheritableThreadLocal.get().setUserDetailInfo(null);
        });
        Object o = submit.get();

        // 父线程拿不到数据
        System.out.println(infoInheritableDataThreadLocal.get().getUserDetailInfo());

		// 因为是深拷贝,更改的不是当前的对象数据
        System.out.println(myInheritableThreadLocal.get().getUserDetailInfo());
        executorService.shutdown();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值