史上最全ThreadLocal 详解

打印 上一主题 下一主题

主题 901|帖子 901|积分 2703

概述

线程本地变量。当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变本身的副本,而不会影响其它线程。
每个线程都有一个 ThreadLocalMap ( ThreadLocal 内部类),Map中元素的键为 ThreadLocal ,而值对应线程的变量副本。

ThreadLocal原理

怎样实现线程隔离

具体关于为线程分配变量副本的代码如下:
  1. public T get() {
  2.     Thread t = Thread.currentThread();
  3.     ThreadLocalMap threadLocals = getMap(t);
  4.     if (threadLocals != null) {
  5.         ThreadLocalMap.Entry e = threadLocals.getEntry(this);
  6.         if (e != null) {
  7.             @SuppressWarnings("unchecked")
  8.             T result = (T)e.value;
  9.             return result;
  10.         }
  11.     }
  12.     return setInitialValue();
  13. }
复制代码

  • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
  • 如果当前线程的threadLocals已经初始化(即不为null) 而且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);
  • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 而且添加到当前线程的threadLocals Map中,并返回
  • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 而且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。
如果存在则直接返回很好理解, 那么对于怎样初始化的代码又是怎样的呢?
  1. private T setInitialValue() {
  2.     T value = initialValue();
  3.     Thread t = Thread.currentThread();
  4.     ThreadLocalMap map = getMap(t);
  5.     if (map != null)
  6.         map.set(this, value);
  7.     else
  8.         createMap(t, value);
  9.     return value;
  10. }
复制代码

  • 首先调用上面写的重载过后的initialValue方法
  • 继续查察当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则创建并添加对象到此中;
同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法
  1. public void set(T value) {
  2.     Thread t = Thread.currentThread();
  3.     ThreadLocalMap map = getMap(t);
  4.     if (map != null)
  5.         map.set(this, value);
  6.     else
  7.         createMap(t, value);
  8. }
复制代码
如许也可以不实现initialValue:
  1. public Connection getConnection() {
  2.     Connection connection = dbConnectionLocal.get();
  3.     if (connection == null) {
  4.         try {
  5.             connection = DriverManager.getConnection("", "", "");
  6.             dbConnectionLocal.set(connection);
  7.         } catch (SQLException e) {
  8.             e.printStackTrace();
  9.         }
  10.     }
  11.     return connection;
  12. }
复制代码
看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时间就从本线程的threadLocals对象中获取就可以了, key就是当前线程;
当然了在当前线程下获取当前线程内里的Map内里的对象并操作肯定没有线程并发题目了, 当然能做到变量的线程隔断离了;
ThreadLocalMap对象是什么

本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与寻常见到的Map有点不一样

  • 它没有实现Map接口;
  • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类
  • ThreadLocalMap的Entry实现继续了WeakReference
  • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket内里仅仅放一个Entry;
要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值:
  1. private void set(ThreadLocal<?> key, Object value) {
  2.     // We don't use a fast path as with get() because it is at
  3.     // least as common to use set() to create new entries as
  4.     // it is to replace existing ones, in which case, a fast
  5.     // path would fail more often than not.
  6.     Entry[] tab = table;
  7.     int len = tab.length;
  8.     int i = key.threadLocalHashCode & (len-1);
  9. //这里用的是Hash冲突的开放定址法的线性探测
  10.     for (Entry e = tab[i];
  11.          e != null;
  12.          e = tab[i = nextIndex(i, len)]) {
  13.         ThreadLocal<?> k = e.get();
  14.         if (k == key) {
  15.             e.value = value;
  16.             return;
  17.         }
  18.         if (k == null) {
  19.             replaceStaleEntry(key, value, i);
  20.             return;
  21.         }
  22.     }
  23.     tab[i] = new Entry(key, value);
  24.     int sz = ++size;
  25.     if (!cleanSomeSlots(i, sz) && sz >= threshold)
  26.         rehash();
  27. }
复制代码
先辈行简单的分析, 对该代码表层意思进行解读:

  • 看下当前threadLocal的在数组中的索引位置 好比: i = 2,看i = 2位置上面的元素(Entry)的Key是否即是threadLocal 这个 Key, 如果即是就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;
  • 如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被接纳了, 那么就调用replaceStaleEntry
  • 如果清理完无用条目(ThreadLocal被接纳的条目)、而且数组中的数据大小 > 阈值的时间对当前的Table进行重新哈希 所以, 该HashMap是处置惩罚辩论检测的机制是向后移位, 清除过期条目 最终找到符合的位置;
了解完Set方法, 后面就是Get方法了:
  1. private Entry getEntry(ThreadLocal<?> key) {
  2.     int i = key.threadLocalHashCode & (table.length - 1);
  3.     Entry e = table[i];
  4.     if (e != null && e.get() == key)
  5.         return e;
  6.     else
  7.         return getEntryAfterMiss(key, i, e);
  8. }
复制代码
先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空而且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找
Entry对象
  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2.     /** The value associated with this ThreadLocal. */
  3.     Object value;
  4.     Entry(ThreadLocal<?> k, Object v) {
  5.         super(k);//父类是WeakReference,也就是相当于new了一个弱引用(k)
  6.         //也就相当于 map中的key是弱引用的
  7.         value = v;
  8.     }
  9. }
复制代码
这里的key指向的ThreadLocal是弱引用,是为了防止ThreadLocal对象永久不会被接纳。因为,若key为强引用,当ThreadLocal不想用了,那么就令 tl = null,但是此时key中还有一个强引用指向ThreadLocal,因此也就永久无法进行接纳(除非ThreadLocalMap不用了),所以会有内存泄露;但如果key使用的是弱引用,只要GC,就会接纳

但是还会有内存泄漏存在,ThreadLocal被接纳,就导致key=null,此时map中也就无法访问到value,无法访问到的value也就无用了,也就是说,这个k-v对无用了,那么value也应该被接纳,但实际上value可能没有被接纳,因此依然存在内存泄露
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严峻效果。
弱引用:GC时,若没有强引用指向这个对象了,只剩下弱引用,就会直接进行接纳。原因就在于GC时无关内存是否足够,弱引用会被直接接纳。所以,只要tl=null了,那么GC时,key指向的ThreadLocal对象就会被接纳
ThreadLocal内存泄漏的原因?

每个线程都有⼀个 ThreadLocalMap 的内部属性,map的key是 ThreaLocal ,定义为弱引用,value是强引用类型。垃圾接纳的时间会⾃动接纳key,而value的接纳取决于Thread对象的生命周期。
一般会通过线程池的方式复用线程节省资源,而如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池内里不会销毁的线程, 内里总会存在着的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严峻, 泄露的内存 = 焦点线程数 * LocalVariable对象的大小;
所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:
  1. private void remove(ThreadLocal<?> key) {
  2.     Entry[] tab = table;
  3.     int len = tab.length;
  4.     int i = key.threadLocalHashCode & (len-1);
  5.     for (Entry e = tab[i];
  6.          e != null;
  7.          e = tab[i = nextIndex(i, len)]) {
  8.         if (e.get() == key) {
  9.             e.clear();
  10.             expungeStaleEntry(i);
  11.             return;
  12.         }
  13.     }
  14. }
复制代码
应用场景

每个线程维护了一个“序列号”
  1. public class SerialNum {
  2.     // The next serial number to be assigned
  3.     private static int nextSerialNum = 0;
  4.     private static ThreadLocal serialNum = new ThreadLocal() {
  5.         protected synchronized Object initialValue() {
  6.             return new Integer(nextSerialNum++);
  7.         }
  8.     };
  9.     public static int get() {
  10.         return ((Integer) (serialNum.get())).intValue();
  11.     }
  12. }
复制代码
Session的管理

Web 应用中的哀求处置惩罚:在 Web 应用中,一个哀求通常会被多个线程处置惩罚,每个线程需要访问本身的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。
经典的另外一个例子:
  1. private static final ThreadLocal threadSession = new ThreadLocal();  
  2.   
  3. public static Session getSession() throws InfrastructureException {  
  4.     Session s = (Session) threadSession.get();  
  5.     try {  
  6.         if (s == null) {  
  7.             s = getSessionFactory().openSession();  
  8.             threadSession.set(s);  
  9.         }  
  10.     } catch (HibernateException ex) {  
  11.         throw new InfrastructureException(ex);  
  12.     }  
  13.     return s;  
  14. }  
复制代码
在线程内部创建ThreadLocal

线程池中的线程对象共享数据:线程池中的线程对象是可以被多个使命共享的,如果线程对象中需要保存使命相关的数据,使用 ThreadLocal 可以保证线程安全。
当然,在使用线程池时,ThreadLocal 可能会导致线程重用时的数据残留,从而影响程序的精确性。因此,在使用线程池时,要确保在使命执行前后清理 ThreadLocal 的值,以避免线程重用时的数据残留。
线程类内部创建ThreadLocal,根本步调如下:

  • 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处置惩罚的对象xxx。
  • 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时间,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
  • 在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,如许可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
  1. public class ThreadLocalTest implements Runnable{
  2.    
  3.     ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();
  4.     @Override
  5.     public void run() {
  6.         String currentThreadName = Thread.currentThread().getName();
  7.         System.out.println(currentThreadName + " is running...");
  8.         Random random = new Random();
  9.         int age = random.nextInt(100);
  10.         System.out.println(currentThreadName + " is set age: "  + age);
  11.         Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值
  12.         Student.setAge(age);
  13.         System.out.println(currentThreadName + " is first get age: " + Student.getAge());
  14.         try {
  15.             Thread.sleep(500);
  16.         } catch (InterruptedException e) {
  17.             e.printStackTrace();
  18.         }
  19.         System.out.println( currentThreadName + " is second get age: " + Student.getAge());
  20.         
  21.     }
  22.    
  23.     private Student getStudentt() {
  24.         Student Student = StudentThreadLocal.get();
  25.         if (null == Student) {
  26.             Student = new Student();
  27.             StudentThreadLocal.set(Student);
  28.         }
  29.         return Student;
  30.     }
  31.     public static void main(String[] args) {
  32.         ThreadLocalTest t = new ThreadLocalTest();
  33.         Thread t1 = new Thread(t,"Thread A");
  34.         Thread t2 = new Thread(t,"Thread B");
  35.         t1.start();
  36.         t2.start();
  37.     }
  38.    
  39. }
  40. class Student{
  41.     int age;
  42.     public int getAge() {
  43.         return age;
  44.     }
  45.     public void setAge(int age) {
  46.         this.age = age;
  47.     }
  48.    
  49. }
复制代码
java 开发手册中保举的 ThreadLocal

看看阿里巴巴 java 开发手册中保举的 ThreadLocal 的用法:
  1. import java.text.DateFormat;
  2. import java.text.SimpleDateFormat;
  3. public class DateUtils {
  4.     public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
  5.         @Override
  6.         protected DateFormat initialValue() {
  7.             return new SimpleDateFormat("yyyy-MM-dd");
  8.         }
  9.     };
  10. }
复制代码
然后再要用到 DateFormat 对象的地方,如许调用:
  1. DateUtils.df.get().format(new Date());
复制代码
面试题专栏

Java面试题专栏已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性题目你不知道怎么答;
那么可以私信我,我会尽我所能帮助你。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

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

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

商道如狼道

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表