栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

juc-04-ThreadLocal

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

juc-04-ThreadLocal

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

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

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

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

public class NoThreadLocalTest {

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

    
    private User getById(Long id) {
 return this.userMap.get(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 + " --" + "身份认证通过");
    }

    
    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 + " --" + "身份认证业务处理");
    }

    
    @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可以修改或者删除requestThread1在userCacheMap` 中存放的数据,这时很不安全的,也很难去维护。

这时,ThreadLocal就闪亮登场了。

2 ThreadLocal

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

2.1 ThreadLocal 怎么用?API
方法 描述
initialValue() 若当前 Thread 没有在 ThreadLocal 中 set 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null。可以重写该方法,设置你想返回的初始值
set(T value) 为当前 Thread 在 ThreadLocal 中 set 新值
T get() 获取当前 Thread 在 ThreadLocal 中的 value,若当前 Thread 没有在 ThreadLocal 中 set 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null。
void remove() 删除当前 Thread 在 ThreadLocal 中对应的 value
2.2 使用 ThreadLocal 设计 第一节中的案例

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

public class UseThreadLocalTest {

    
    private Map userMap;

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

    
    private User getById(Long id) {
 return this.userMap.get(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 + " --" + "身份认证通过");
    }

    
    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();
    }

    
    @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 原理详解

Thread,ThreadLocal 以及 ThreadLocalMap 三者之间的关系:

  • 每个Thread对象中都持有一个 ThreadLocalMap 类型的成员变量 threadLocals。
  • ThreadLocalMap 中的有一个Entry数组(Entry[]),key 是 ThreadLocal实例,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> {
     
     Object value;

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

 
 private static final int INITIAL_CAPACITY = 16;

 
 private Entry[] table;
    }
...

}

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

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

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号