ThreadLocal笔记

并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
如果使用锁来保证资源隔离,会存在大量锁等待,会让响应时间延长很多。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

ThreadLoacl原理

public class ThreadLocal<T> {
     ...
     public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     static class ThreadLocalMap {
        ...
     }
     ...
}

ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
   }
   ...
   private Entry[] table;
   ...
}

ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。

ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。

而ThreadLocalMap被定义成了Thread类的成员变量。

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

在这里插入图片描述

Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。

ThreadLocalMap的Entry为什么用ThreadLocal做key?

如果一个线程只使用一个ThreadLocal对象,使用Thread做key是可以的。但是一个线程中不可能只使用一个ThreadLocal对象,再使用Thread做key就有问题

Entry的key为什么设计成弱引用?

弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

线程很多情况下是复用的,一个线程执行多个任务。一个任务执行完如果不把key设置为nul会一直存在,从而造成内存泄漏。

Entry的value为什么不设计成弱引用?

Entry的value假如只是被Entry引用,有可能没被其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致出现异常。

ThreadLocal真的会导致内存泄露?

强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object

若Thread为工作线程,长期存在,会导致内存泄漏

如何解决内存泄露问题?

在finally代码块中,调用remove方法清理没用的数据,remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

父子线程如何共享数据?

ThreadLocal都是在一个线程中保存和获取数据的。

 @Test
    public void test1() {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
    

在这里插入图片描述
使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

  @Test
    public void test2(){
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }

在这里插入图片描述
InheritableThreadLocal的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

线程池中如何共享数据?

   public void test3(){
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        threadLocal.set(6);
        executorService.submit(() -> {
            System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
        });

        threadLocal.set(7);
        executorService.submit(() -> {
            System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
        });
    }

在这里插入图片描述

由于这个例子中使用了单例线程池,固定线程数是1。

第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。

因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

使用阿里巴巴的一个开源jar包

   <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.14.2</version>
        </dependency>
@Test
    public void test4(){
        TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

        threadLocal.set(6);
        ttlExecutorService.submit(() -> {
            System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
        });

        threadLocal.set(7);
        ttlExecutorService.submit(() -> {
            System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
        });
    }

在这里插入图片描述

ThreadLocal为什么建议用static修饰?

static修饰的变量是在类在加载时就分配地址了,在类卸载才会被回收

如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tcoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值