为了账号安全,请及时绑定邮箱和手机立即绑定

juc-04-ThreadLocal

标签:
Java

工作中,相信很多朋友都听过或者用过 ThreadLocal。这篇文章就来说说它是什么?具体怎么玩?还有分析 ThreadLocal 常用API对应的源码。

1 场景案例:演示身份认证到业务处理

在生产环境中,通常是多个请求并发请求到服务器,一般会经过“身份认证与鉴权”和“业务处理”两个步骤。

下面我们来模拟一下大概的实现逻辑,相信大多数朋友的项目中,代码逻辑类似下面的代码:

public class NoThreadLocalTest {

    /**
     * 用 map 模拟数据源
     */
    private Map userMap;

    /**
     * 执行测试用例前,初始化数据源
     */
    @Before
    public void initUserList() {
        userMap = new ConcurrentHashMap<>();
        User u1 = new User(1, "陈一");
        User u2 = new User(2, "钱二");
        User u3 = new User(3, "张三");
        User u4 = new User(4, "李四");
        User u5 = new User(5, "王五");
        User u6 = new User(6, "赵六");
        userMap.put(u1.getId(), u1);
        userMap.put(u2.getId(), u2);
        userMap.put(u3.getId(), u3);
        userMap.put(u4.getId(), u4);
        userMap.put(u5.getId(), u5);
        userMap.put(u6.getId(), u6);
    }

    /**
     * 模拟从数据库中,根据 id 查询 user
     *
     * @param id
     * @return
     */
    private User getById(Long id) {
        return this.userMap.get(id);
    }

    /**
     * 模拟项目的 Filter 或者 Interceptor 层,身份认证和鉴权环节
     *
     * @param id
     */
    private void doAuth(Long id) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行身份认证");
        // 从数据库中,根据 id 查询 user
        User user = this.getById(id);
        if (user == null)
            throw new NullPointerException("user is null");
        // 省略其他校验逻辑
        // 模拟身份认证处理的耗时,这里设置 50ms
        TimeUnit.MILLISECONDS.sleep(50);
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证通过");
    }

    /**
     * 模拟项目的 service 层,进行业务处理
     *
     * @param id
     */
    private void doService(Long id) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行业务处理");
        // 再次从数据库中,根据 id 查询 user
        User user = this.getById(id);
        if (user == null)
            throw new NullPointerException("user is null");
        // 省略其他业务处理逻辑
        // 模拟业务处理的耗时,这里设置 100ms
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证业务处理");
    }

    /**
     * 测试不使用 ThreadLocal,当需要使用 user 时,每次都从数据库查询用户信息
     */
    @Test
    public void test() throws InterruptedException {
        List threads = new ArrayList<>();
        // 新建 6 条请求线程,模拟生产环境中并发请求场景
        for (long i = 1; i <= this.userMap.size(); i++) {
            long id = i;
            Thread requestThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 模拟项目中,每个请求都要进行 身份认证和业务处理 两步逻辑
                        doAuth(id);
                        doService(id);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads.add(requestThread);
        }
        // 启动所有的请求线程
        for (Thread thread : threads) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(5);
    }

}

运行结果,正常:

Thread-0-- 1 --开始进行身份认证
Thread-1-- 2 --开始进行身份认证
Thread-2-- 3 --开始进行身份认证
Thread-3-- 4 --开始进行身份认证
Thread-4-- 5 --开始进行身份认证
Thread-5-- 6 --开始进行身份认证
Thread-5-- 6 --身份认证通过
Thread-5-- 6 --开始进行业务处理
Thread-3-- 4 --身份认证通过
Thread-2-- 3 --身份认证通过
Thread-2-- 3 --开始进行业务处理
Thread-1-- 2 --身份认证通过
Thread-1-- 2 --开始进行业务处理
Thread-0-- 1 --身份认证通过
Thread-0-- 1 --开始进行业务处理
Thread-4-- 5 --身份认证通过
Thread-3-- 4 --开始进行业务处理
Thread-4-- 5 --开始进行业务处理
Thread-1-- 2 --身份认证业务处理
Thread-4-- 5 --身份认证业务处理
Thread-3-- 4 --身份认证业务处理
Thread-5-- 6 --身份认证业务处理
Thread-0-- 1 --身份认证业务处理
Thread-2-- 3 --身份认证业务处理

分析:
处理结果正常,但是代码中我们可以看到,在 doAuth(id)doService(id) 这两个方法中,都执行了 this.getById(id); 根据 id 查询数据库。这是一个重复的操作,每次操作数据库,都会存在网络延迟,对数据库也会产生查询压力,同时服务器端要建立数据库连接,也需要消耗资源和性能

那有没有什么办法可以避免多次 getById(id) 查库呢?
从业务逻辑上,当服务到接收到请求时,会分配一个线程(后面用 requestThread表示)专门来处理该请求。身份认证环节 doAuth(id) 中通过 getById(id) 查询到用户信息 user,当认证通过说明当前请求的是合法的用户,同时,我们也已经获取到了用户信息,请求线程 requestThread中的后续操作如:doService(id) ,我们可以想办法直接使用身份认证环节查询到的 user,而不用再次 getById(id) 查库。
这时,大家应该都会想到一种的数据结构,Map 或者存取操作线程安全的 ConcurrentHashMap
比如:Map userCacheMap = new ConcurrentHashMap<>();

但是,userCacheMap 是一个公共资源,多个线程都能够同时操作这个 ``ConcurrentHashMap,比如requestThread2可以修改或者删除requestThread1userCacheMap` 中存放的数据,这时很不安全的,也很难去维护。

这时,ThreadLocal就闪亮登场了。

2 ThreadLocal

>ThreadLocal的用法类似 Map(内部实现原理和 Map不一样,下面会分析),而且,所有线程的操作都是线程隔离的,也就是说每个线程只能操作自己线程相关的资源,通过 get()set()remove() 等方法操作的都是当前线程对应的值,线程安全。

2.1 ThreadLocal 怎么用?API

方法 描述
initialValue() 若当前 Thread 没有在 ThreadLocalset 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null。可以重写该方法,设置你想返回的初始值
set(T value) 为当前 ThreadThreadLocalset 新值
T get() 获取当前 ThreadThreadLocal 中的 value,若当前 Thread 没有在 ThreadLocalset 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null
void remove() 删除当前 ThreadThreadLocal 中对应的 value

2.2 使用 ThreadLocal 设计 第一节中的案例

在代码中,设置一个公共的 ThreadLocal 变量,用于保存各个请求线程中的资源,各个线程的操作的都是线程隔离的。

public class UseThreadLocalTest {

    /**
     * 用 map 模拟数据源
     */
    private Map userMap;

    /**
     * threadlocal,用于保存各个请求线程中的资源
     */
    private ThreadLocal userThreadLocal = new ThreadLocal<>();

    /**
     * 执行测试用例前,初始化数据源
     */
    @Before
    public void initUserList() {
        userMap = new ConcurrentHashMap<>();
        User u1 = new User(1, "陈一");
        User u2 = new User(2, "钱二");
        User u3 = new User(3, "张三");
        User u4 = new User(4, "李四");
        User u5 = new User(5, "王五");
        User u6 = new User(6, "赵六");
        userMap.put(u1.getId(), u1);
        userMap.put(u2.getId(), u2);
        userMap.put(u3.getId(), u3);
        userMap.put(u4.getId(), u4);
        userMap.put(u5.getId(), u5);
        userMap.put(u6.getId(), u6);
    }

    /**
     * 模拟从数据库中,根据 id 查询 user
     *
     * @param id
     * @return
     */
    private User getById(Long id) {
        return this.userMap.get(id);
    }

    /**
     * 模拟项目的 Filter 或者 Interceptor 层,身份认证和鉴权环节
     *
     * @param id
     */
    private void doAuth(Long id) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行身份认证");
        // 从数据库中,根据 id 查询 user
        User user = this.getById(id);
        if (user == null)
            throw new NullPointerException("user is null");
        // 省略其他校验逻辑
        // 模拟身份认证处理的耗时,这里设置 50ms
        TimeUnit.MILLISECONDS.sleep(50);

        // 身份认证通过后,将 user 缓存到 ThreadLocal 中
        this.userThreadLocal.set(user);
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证通过");
    }

    /**
     * 模拟项目的 service 层,进行业务处理
     *
     * @param id
     */
    private void doService(Long id) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行业务处理");
        // 因为在身份认证通过后,我们已经把 id 对应的 user 信息缓存到了 ThreadLocal 中,所以,这里我们只需要从 threadLocal get 出 id 对应的 user
        User user = this.userThreadLocal.get();
        if (user == null)
            throw new NullPointerException("user is null");
        // 省略其他业务处理逻辑
        // 模拟业务处理的耗时,这里设置 100ms
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证业务处理");
        // 当前请求业务处理完成后,将 ThreadLocal 中缓存的当前线程的数据删除
        this.userThreadLocal.remove();
    }

    /**
     * 使用 ThreadLocal,身份认证通过时,用 ThreadLocal 缓存 user ,
     * 再当前线程中,其他缓解需要用到 user 信息时,从 ThreadLocal 中直接 get(),
     * 请求线程结束时,再删除ThreadLocal中当前线程的数据
     */
    @Test
    public void test() throws InterruptedException {
        List threads = new ArrayList<>();
        // 新建 6 条请求线程,模拟生产环境中并发请求场景
        for (long i = 1; i <= this.userMap.size(); i++) {
            long id = i;
            Thread requestThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 模拟项目中,每个请求都要进行 身份认证和业务处理 两步逻辑
                        doAuth(id);
                        doService(id);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads.add(requestThread);
        }
        // 启动所有的请求线程
        for (Thread thread : threads) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(5);
    }

}

运行依然线程是线程安全的,每个线程在 doService(id) 方法中都能通过 ThreadLocal 读取到当前线程在 doAuth(id) 方法设置的 user,避免了多次 getById(id) 查询数据库。

2.3 ThreadLocal 原理详解

ThreadThreadLocal 以及 ThreadLocalMap 三者之间的关系:

  • 每个Thread对象中都持有一个 ThreadLocalMap 类型的成员变量 threadLocals
  • ThreadLocalMap 中的有一个Entry数组(Entry[]),keyThreadLocal实例,value 是线程在ThreadLocal.set(value) 中设置的 value

原理图:
图片描述

2.4 源码分析

Thread类中ThreadLocalMap

Thread类中有一个 ThreadLocalMap 类型的成员变量 threadLocals

public class Thread implements Runnable {
...
    // Thread 类中持有一个 ThreadLocalMap 成员变量
    ThreadLocal.c threadLocals = null;
...
}

ThreadLocal的部分源码


public class ThreadLocal {

...
    // 获取当前 `Thread` 在 `ThreadLocal` 中的 `value`
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // map为null时,新建map,通过initialValue()设置并返回初始值
        return setInitialValue();
    }
    
    // 获取 Thread 中的 ThreadLocalMap 变量
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // 设置初始值
    private T setInitialValue() {
        // 获取初始值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 设置当前线程在ThreadLocal的值为初始值
            map.set(this, value);
        else
            // Thread 的 ThreadLocalMap 变量初始化,并设置初始值
            createMap(t, value);
        return value;
    }

    // 获取初始值的方法,可重写
    protected T initialValue() {
        return null;
    }
    
    // 当前 Thread 在 ThreadLocal 中 set 新值                                                                                                                                               
    public void set(T value) {
        // 当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    // Thread 的 ThreadLocalMap 变量初始化,并设置初始值
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    // 删除当前 `Thread` 在 `ThreadLocal` 中对应的 `value`                                                                                                                                          
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

...

    // 看看 ThreadLocal 中 ThreadLocalMap 的类结构
    static class ThreadLocalMap {

        // Entry 是一种 弱引用
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * Entry[] 的初始容量 16
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * Entry数组,每个Entry都保存了 key(ThreadLocal) 和 value
         */
        private Entry[] table;
    }
...

}

这篇文章,通过模拟实际项目中的一个场景,给大家演示了 ThreadLocal 的使用,它能保证各个线程对 ThreadLocal的操作的都是线程隔离的,从而保证线程安全,安全地保存当前线程的数据。
同时,也列举和通过源码分析了 ThreadLocal 各个API的使用。

>代码:
>github.com/wengxingxia/002juc.git

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
10
获赞与收藏
8

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消