ThreadLocal 解析

使用场景

典型场景一

每个线程需要一个独享的对象 (通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)

1000 个打印线程的任务,都用线程池来运行

在这里插入图片描述
避免众多对象创建和销毁的开销,把 SimpleDateFormat 提出来作为公共变量,但这就会产生线程安全问题,解决方案可以加锁,但是太影响性能了,更好的解决方案使用 ThreadLocal,每个 Thread 内有自己的实例副本不共享,利用 ThreadLocal,给每个线程分配自己的 dateFormat 对象,保证了线程安全,高效利用内存
在这里插入图片描述

public class ThreadLocalTest1 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args){
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalTest1().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeForMatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

}

class ThreadSafeForMatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
}

SimpleDateFormat的进化之路

  1. 2个线程分别用自己的 SimpleDateFormat,这没问题
  2. 后来延伸出10个,那就有10个线程和10个SimpleDateFormat ,这虽然写法不优雅(应该复用对象),但勉强可以接受
  3. 但是当需求变成了 1000 个,那么必然要用线程池(否则消耗内存太多)
  4. 所有的线程都共用同一个 simpleDateFormat 对象
  5. 这是线程不安全的,出现了并发安全问题
  6. 我们可以选择加锁 ,加锁后结果正常,但是效率低
  7. 在这里更好的解决方案是使用 ThreadLocal

典型场景二

每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦,强调的是同一个请求内(同一个线程内)不同方法间的共享,不需重写 initialValue() 方法,但是必须手动调用 set() 方法
在这里插入图片描述
用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)。这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的

当多线程同时工作时,我们需要保证线程安全,可以用 synchronized 也可以用 ConcurrentHashMap ,但无论用什么都会对性能有所影响

更好的办法是使用ThreadLocal ,这样无需synchronized ,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的

在线程生命周期内,都通过这个静态 Threadlocal 实例的 get() 方法取得自己set 过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦

在这里插入图片描述

public class ThreadLocalTest2 {
    public static void main(String[] args) {
        new Service1().process("");
    }
}

class Service1 {
    public void process(String name) {
        User user = new User("邦哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeForMatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

运行结果

Service2拿到用户名:超哥
Service3拿到用户名:超哥

Process finished with exit code 0

两种使用场景殊途同归

通过源码可以看出, setInitialValue 和直接 set 最后都是利用 map.set() 方法来设置值也就是说,最后都会对应到ThreadLocalMap的一个Entry ,只不过是起点和入口不一样

作用

1.让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)

2.在任何方法中都可以轻松获取到该对象

根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象

场景一中:在 ThreadLocal 第一次 get 的时候把对象给初始化出来,对象的初始化时机可以由我们控制

场景二中:如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们 ThreadLocal 中去,以便后续使用。

原理

搞清楚Thread、ThreadLocal以及ThreadLocalMap三者之间的关系,每个Thread对象中都持有一个ThreadLocalMap成员变量

在这里插入图片描述

方法

initialValue( )

该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get 的时候,才会触发,ThreadLocal 的 get 方法中调用了 initialValue() 方法

在这里插入图片描述
在这里插入图片描述

当线程第一次使用 get 方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法
在这里插入图片描述

通常,每个线程最多调用一次 initialValue 此方法,但如果已经调用了 remove() 后,再调用 get(),则可以再次调用此方法

如果不重写本方法这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法,以便在后续使用中可以初始化副本对象。

set()

为这个线程设置一个新值

get()

get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出map 中属于本 ThreadLocal 的 value

注意,这个 map 以及 map 中的 key 和 value 都是保存在线程中的,而不是保存在 ThreadLocal 中

remove( )

删除对应这个线程的值

initialValue()

是没有默认实现的,如果我们要用 initialValue 方法,需要自己实现,通常是匿名内部类的方式

ThreadLocalMap

ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table,可以认为是一个 map

键:这个ThreadLocal

值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象

处理冲突: ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突就继续找下一个空位置而不是用链表拉链

内存泄漏

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收

Key 的泄漏:ThreadLocalMap 中的 Entry 继承自 WeakReference,是弱弓|用

弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联) , 那么这个对象就可以被回收

所以弱弓|用不会阻止GC ,因此这个弱弓|用的机制

Value的泄漏

ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时每个 Entry 都包含了一个对 value 的强引|用

正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了

但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,因为有以下的调用链:
在这里插入图片描述
因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现 OOM

JDK 已经考虑到了这个问题,所以在 set, remove, rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收

但是如果一个ThreadLocal 不被使用,那么实际上 set, remove,rehash 方法也不会被调用,如果同时线程又不停止,那么调用链
就一直存在,那么就导致了 value 的内存泄漏

如何避免内存泄露 (阿里规约):调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值