《深入解析 Java 中的 ThreadLocal》

打印 上一主题 下一主题

主题 1003|帖子 1003|积分 3009

ThreadLocal

1.概述

ThreadLocal被称为线程局部变量,用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程,以是该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal用于在同一个线程间,在不同的类和方法之间共享数据的的场景,也可以用于在不同线程间隔离数据的场景。
ThreadLocal使用Thread中的ThreadLocalMap来举行数据存储。

示例:
  1. public class Test {
  2.         // 线程局部变量
  3.         public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
  4.         public static void main(String[] args) {
  5.                 Thread t1 = new Thread(new Runnable() {
  6.                         @Override
  7.                         public void run() {
  8.                                 threadLocal.set("妲己");
  9.                                 show();
  10.                                 Sample.dosth();
  11.                         }
  12.                 },"线程1");
  13.                 Thread t2 = new Thread(new Runnable() {
  14.                         @Override
  15.                         public void run() {
  16.                                 threadLocal.set("后羿");
  17.                                 show();
  18.                                 Sample.dosth();
  19.                         }
  20.                 },"线程2");
  21.                 t1.start();
  22.                 t2.start();
  23.         }
  24.         public static void show() {
  25.                 String role = threadLocal.get();
  26.                 System.out.println("show:"+Thread.currentThread().getName()+"分配角色:" + role);
  27.         }
  28. }
  29. class Sample {
  30.         public static void dosth() {
  31.                 String role = Test.threadLocal.get();
  32.                 System.out.println("dosth:"+Thread.currentThread().getName()+"分配角色:" + role);
  33.         }
  34. }
复制代码
2.常用方法

1.存储数据至当前线程的ThreadLocalMap:public void set(T value)
  1. public void set(T value) {
  2.      // 获取当前线程
  3.      Thread t = Thread.currentThread();
  4.         // 获取当前线程的ThreadLocalMap
  5.      ThreadLocalMap map = getMap(t);
  6.         // 存储数据至当前线程的ThreadLocalMap中
  7.         // 使用ThreadLocal对象做key,保存数据value
  8.      if (map != null)
  9.          map.set(this, value);
  10.      else
  11.          createMap(t, value);
  12. }
复制代码
2.从当前线程的ThreadLocalMap中获取数据:public T get()
  1. public T get() {
  2.      // 获取当前线程的ThreadLocalMap
  3.      Thread t = Thread.currentThread();
  4.      ThreadLocalMap map = getMap(t);
  5.      // 使用ThreadLocal对象做key,获取数据(Entry类型)
  6.      if (map != null) {
  7.          ThreadLocalMap.Entry e = map.getEntry(this);
  8.          if (e != null) {
  9.              @SuppressWarnings("unchecked")
  10.              T result = (T)e.value;
  11.              return result;
  12.          }
  13.      }
  14.      return setInitialValue();
  15. }
复制代码
3.从当前线程的ThreadLocalMap中删除数据:public void remove()
在线程池的线程复用场景中,线程执行完毕时一定要调用remove(),避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
  1. public void remove() {
  2.       // 获取当前线程的ThreadLocalMap
  3.       ThreadLocalMap m = getMap(Thread.currentThread());
  4.       // 使用当前ThreadLocal对象做key,删除数据
  5.       if (m != null)
  6.           m.remove(this);
  7. }
复制代码
ThreadLocal的get()方法、set()方法和remove()方法,实在最终操作的都是ThreadLocalMap类中的数据。
3. ThreadLocalMap内部结构

ThreadLocalMap内部数据结构是一个Entry范例的数组。每个Entry对象的key为ThreadLocal对象,value为存储的数据。
  1. static class ThreadLocalMap {
  2.    static class Entry extends WeakReference<ThreadLocal<?>> {
  3.        Object value;
  4.        Entry(ThreadLocal<?> k, Object v) {
  5.            super(k);
  6.            value = v;
  7.        }
  8.    }
  9.    ...
  10.    private Entry[] table;
  11.    ...
  12. }
复制代码

4.为什么用ThreadLocal做key?

假如在应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也是可以的,代表每个Thread线程对应一个value。
  1. public class ThreadLocalService {
  2.    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  3. }   
复制代码
但是,在实际应用步伐开发过程中,一个线程中很有大概不只使用了一个ThreadLocal对象。这时使用Thread做key就会产生混淆。
  1. public class ThreadLocalService {
  2.    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
  3.    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
  4.    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
  5. }
复制代码

以是,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get()方法,获取到当前线程的ThreadLocalMap,然后进一步获取到对应的Entry。

5.ThreadLocalMap如何查找数据?

​ 当我们使用ThreadLocal获取当前线程中保存的Value数据时,是以ThreadLocal对象作为Key,通过访问Thread当前线程对象的内部
ThreadLocalMap集合来获取到Value。
ThreadLocalMap集合的底层数据结构使用Entry[]数组保存Key-Value键值对数据。以是,当通过ThreadLocal的get、set()、remove()等
方法,访问ThreadLocalMap时,最终都会通过一个下标,来完成对数组中的元素访问。
  1. int i = key.threadLocalHashCode & (len-1);
复制代码
​ 通过key的“hashCode值”跟"数组的长度减1" 做“&按位与”运算。其中key就是ThreadLocal对象。这种计算方式,相当于用“hashCode值”跟“数组的长度”举行“%取余”运算。
​ 假设:len=16,key.threadLocalHashCode=31
​ hash & len-1的计算结果与hash % len的计算结果一直,均为15,但是“&按位与”运算的效率更高。
6.父子线程如何共享数据?

在实际工作中,有大概需要在父子线程中共享数据的。即:在父线程中往ThreadLocal设置了值,在子线程中能够获取到。
  1.         ThreadLocal<String> threadLocal = new ThreadLocal<String>();
  2.         threadLocal.set("天王盖地虎");
  3.         System.out.println("main主线程:" + threadLocal.get());
  4.         Thread thread = new Thread(new Runnable() {
  5.             @Override
  6.             public void run() {
  7.                 System.out.println("子线程:" + threadLocal.get());
  8.             }
  9.         });
  10.         thread.start();
  11.     }
复制代码
​ 在这种环境下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。两个线程对象,各自拥有不同的ThreadLocalMap。应该使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
  1.         InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();
  2.         threadLocal.set("天王盖地虎");
  3.         System.out.println("main主线程:" + threadLocal.get());
  4.         Thread thread = new Thread(() -> {
  5.             System.out.println("子线程:" + threadLocal.get());
  6.         });
  7.         thread.start();
  8.     }
复制代码
7.ThreadLocal如何避免内存泄露?

在finally调用ThreadLocal对象的remove()方法。
​ 需要特别注意的地方是:一定要在finally代码块中,调用remove()方法清理没用的数据。假如业务代码出现非常,也能及时清理没用的数据。
​ remove()方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,以是它能解决内存泄露题目。
8.ThreadLocal应用场景

1线程数据隔离
​ ThreadLocal的紧张代价在于线程隔离,ThreadLocal中的数据只属于当前线程,该数据对别的线程是不可见的,起到隔离作用。这样操作,可以在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提拔了并发性的性能。
​ 例如:SqlSession会话对象绑定,避免多个线程使用同一个SqlSession对象,由于关闭导致非常。
  1.     private static final ThreadLocal threadSession = new ThreadLocal();
  2.     public static SqlSession getSession() {
  3.         SqlSession s = (SqlSession) threadSession.get();
  4.         if (s == null) {
  5.             s = getSqlSessionFactory().openSqlSession();
  6.             threadSession.set(s);
  7.         }
  8.     }
复制代码
2跨函数通报
​ 数据通常用于同一个线程内,跨类、跨方法通报数据时,假如不用ThreadLocal,那么相互之间的数据通报势必要靠返回值和参数,这样无形之中增长了这些类或者方法之间的耦合度.
  1. // SpringMVC或SpringBoot框架使用中获取HttpServletRequest
  2.     HttpServletRequest request =
  3.             ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
复制代码
  1.    @Nullable
  2.     public static RequestAttributes getRequestAttributes() {
  3.         // 获取当前线程中的存储的Request Attribute
  4.         RequestAttributes attributes = requestAttributesHolder.get();
  5.         if (attributes == null) {
  6.             attributes = inheritableRequestAttributesHolder.get();
  7.         }
  8.         return attributes;
  9.     }
  10.     private static final ThreadLocal<RequestAttributes> requestAttributesHolder =  new NamedThreadLocal<>("Request attributes");
  11.     private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =  new NamedInheritableThreadLocal<>("Request context");
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

农妇山泉一亩田

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表