多线程ThreadLocal详细介绍

401 阅读12分钟

前言:进程中存在多个线程,该进程中多线程可以资源共享,但是多个线程多统一个资源进行修改时,会造成线程安全问题,我们有没有办法解决呢?于是有了ThreadLocal,我的理解为每一个线程创建一个副本,每个线程专用。

一 ThreadLocal介绍

  • ThreadLocal类通常被翻译为线程本地变量类或者线程局部变量类。
  • 在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。

二 ThreadLocal基本使用

  • ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。
  • 线程本地变量可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。

案例代码

package ThreadLocal;
​
import ExecutorDemo.newFixedThreadPool.FixedThreadPoolDemo;
​
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
​
/**
 * @description:
 * @author: shu
 * @createDate: 2022/11/2 20:19
 * @version: 1.0
 */
public class ThreadLocalDemo {
​
    static class Foo{
        // 计数器
        static final AtomicInteger AMOUT=new AtomicInteger();
        int index=0;
        int bar=10;
​
        public Foo() {
            this.index=AMOUT.getAndIncrement();
        }
​
        public int getIndex() {
            return index;
        }
​
        public void setIndex(int index) {
            this.index = index;
        }
​
        public int getBar() {
            return bar;
        }
​
        public void setBar(int bar) {
            this.bar = bar;
        }
​
​
        @Override
        public String toString() {
            return "Foo{" +
                    "index=" + index +
                    ", bar=" + bar +
                    '}';
        }
    }
​
    private static final ThreadLocal<Foo>  FOO_THREAD_LOCAL=new ThreadLocal<>();
​
    public static void main(String[] args) throws InterruptedException {
        // 线程池
        ExecutorService singleThreadExecutor = Executors.newFixedThreadPool(5);
        // 批量添加线程
        for (int i = 0; i < 5; i++) {
            singleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    // 本地线程变量中没有值
                    if(FOO_THREAD_LOCAL.get()==null){
                        FOO_THREAD_LOCAL.set(new Foo());
                    }
                    System.out.println("FOO_THREAD_LOCAL 初始化的值"+FOO_THREAD_LOCAL.get());
​
                    for (int j = 0; j < 10; j++) {
                        Foo foo = FOO_THREAD_LOCAL.get();
                        foo.setBar(foo.getBar()+1);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    
                    System.out.println("FOO_THREAD_LOCAL 累加的值"+FOO_THREAD_LOCAL.get());
​
                    // 删除当前本地线程变量绑定的值
                    FOO_THREAD_LOCAL.remove();
                }
            });
​
        }
        Thread.sleep(1000);
        // 线程池销毁
        singleThreadExecutor.shutdown();;
​
    }
}
​

在线程本地变量(FOO_THREAD_LOCAL)中,每一个线程都绑定了一个独立的值(Foo对象),这些值对象是线程的私有财产,可以理解为线程的本地值,线程的每一次操作都是在自己的同一个本地值上进行的,从例子中线程本地值的index始终一致可以看出,每个线程操作的是同一个Foo对象。

三 ThreadLocal使用场景

ThreadLocal是解决线程安全问题的一个较好的方案,它通过为每个线程提供一个独立的本地值去解决并发访问的冲突问题。

线程隔离

ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。 场景:可以为每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源。

     private static final ThreadLocal threadSession = new ThreadLocal();  
​
     //获取会话,如果Session的使用方式为共享而不是独占,在
    //这种情况下,Session是多线程共享使用的,
    //如果某个线程使用完成之后直接将Session关闭,
    //其他线程在操作Session时就会报错。
     public static Session getSession() throws InfrastructureException {  
         Session s = (Session) threadSession.get();
         try {
             if (s == null) {
                 s = getSessionFactory().openSession();
                 threadSession.set(s);
             }  
         } catch (HibernateException ex) {
             throw new InfrastructureException(ex);
         }  
         return s;  
     }

跨函数传递数据

通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。 场景:可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。

​
     public class SessionHolder
     {
         
         // session id,线程本地变量
         private static final ThreadLocal<String> sidLocal =
                                               new ThreadLocal<>("sidLocal");
​
         
         // 用户信息,线程本地变量
         private static final ThreadLocal<UserDTO> sessionUserLocal =
                                                new ThreadLocal<>("sessionUserLocal");
​
         
         // session,线程本地变量
         private static final ThreadLocal<HttpSession> sessionLocal = 
                                                   new ThreadLocal<>("sessionLocal");
​
     
         /**
          *保存session在线程本地变量中
           */
         public static void setSession(HttpSession session)
         {
             sessionLocal.set(session);
         }
     
         /**
          * 取得绑定在线程本地变量中的session 
           */
         public static HttpSession getSession()
         {
             HttpSession session = sessionLocal.get();
             Assert.notNull(session, "session未设置");
             return session;
         }
        
     }

四 新老ThreadLocal比较

早期Jdk

  • 早期版本中的Map结构,其拥有者为ThreadLocal,每一个ThreadLocal实例拥有一个Map实例。
  • 在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在线程本地变量中绑定的值为Value(本地值)。

注意:key:为当前线程,value:ThreadLocal绑定的值

Jdk1.8

  • 在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例
  • 在JDK 8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap),如果给一个Thread创建多个ThreadLocal实例,然后放置本地数据,那么当前线程的ThreadLocalMap中就会有多个Key-Value对,其中ThreadLocal实例为Key,本地数据为Value。

总结

  • 拥有者发生了变化:新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal。
  • Key发生了变化:新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。

优势

  • 每个ThreadLocalMap存储的Key-Value对数量变少。早期版本的Key-Value对数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储的Key-Value对数量也多。新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。
  • 每个ThreadLocalMap存储的Key-Value对数量变少。早期版本的Key-Value对数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储的Key-Value对数量也多。新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。

五 ThreadLocal源码分析

get()方法

get()方法用于获取线程本地变量在当前线程的ThreadLocalMap中对应的值,相当于获取线程本地值

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程对象的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 判断
        if (map != null) {
            // 已当前线程为key,获取value
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 存在,就返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 没有存在,就初始值
        return setInitialValue();
    }
​
​
​
private T setInitialValue() {
        // 调用初始化函数,NULL    
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
​
​
// 创建新ThreadLocalMap成员
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. 先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
  2. 如果获得的map不为空,那么以当前ThreadLocal实例为Key尝试获得map中的Entry(条目)。
  3. 如果Entry不为空,就返回Entry中的Value。
  4. 如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。
  5. 如果map不存在,还会给当前线程创建新ThreadLocalMap成员,并绑定第一个Key-Value对。

set()方法

set(T value)方法用于设置线程本地变量在当前线程的ThreadLocalMap中对应的值,相当于设置线程本地值

          public void set(T value) {
             //获取当前线程对象
             Thread t = Thread.currentThread();
             
             //获取当前线程的ThreadLocalMap 成员
             ThreadLocalMap map = getMap(t);
             
             //判断map是否存在
             if (map != null)
             { 
                 //value被绑定到threadLocal实例
                 map.set(this, value);
             }
             else
             {
                 // 如果当前线程没有ThreadLocalMap成员实例
                 // 创建一个ThreadLocalMap实例,然后作为成员关联到t(thread实例)
                 createMap(t, value);
             }
         }
     
         // 获取线程t的ThreadLocalMap成员
         ThreadLocalMap getMap(Thread t) {
             return t.threadLocals;
         }
         
         // 线程t创建一个ThreadLocalMap成员
         // 并为新的Map成员设置第一个“Key-Value对”,Key为当前的ThreadLocal实例
         void createMap(Thread t, T firstValue) {
             t.threadLocals = new ThreadLocalMap(this, firstValue);
         }
​
  1. 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
  2. 如果map不为空,就将Value设置到map中,当前的ThreadLocal作为Key。
  3. 如果map为空,为该线程创建map,然后设置第一个Key-Value对,Key为当前的ThreadLocal实例,Value为set()方法的参数value值。

remove()方法

remove()方法用于在当前线程的ThreadLocalMap中移除“线程本地变量”所对应的值。

          public void remove() {
              ThreadLocalMap m = getMap(Thread.currentThread());
              if (m != null)
                  m.remove(this);
          }

initialValue()方法

当线程本地变量在当前线程的ThreadLocalMap中尚未绑定值时,initialValue()方法用于获取初始值。

        protected T initialValue() {
             return null;
         }
  • 默认情况下,initialValue()方法返回null,如果不想返回null,可以继承ThreadLocal以覆盖此方法。

初始化方法

     ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());
​
        //ThreadLocal工厂方法可以设置本地变量初始值钩子函数
         public static <S> ThreadLocal<S> withInitial(
                                     Supplier<? extends S> supplier) {
             return new SuppliedThreadLocal<>(supplier);
         }
     
         //内部静态子类
         //继承了ThreadLocal,重写了initialValue()方法,返回钩子函数的值作为初始值
         static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
             //保存钩子函数
             private final Supplier<? extends T> supplier;
             //传入钩子函数
             SuppliedThreadLocal(Supplier<? extends T> supplier) {
                 this.supplier = Objects.requireNonNull(supplier);
             }
     
             @Override
             protected T initialValue() {
                 return supplier.get();  //返回钩子函数的值作为初始值
             }
         }

六 ThreadLocalMap源码分析

ThreadLocal的操作都是基于ThreadLocalMap展开的,而ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)。

成员变量

     public class ThreadLocal<T> {

         
     static class ThreadLocalMap {  
             // Map的条目数组,作为哈希表使用
             private Entry[] table;
             // Map的条目初始容量16
             private static final int INITIAL_CAPACITY = 16;
             // Map的条目数量 
             private int size = 0;
             // 扩容因子
             private int threshold; 
             // Map的条目类型,一个静态的内部类
             // Entry 继承子WeakReference, Key为ThreadLocal实例
             static class Entry extends WeakReference<ThreadLocal<?>> {
                 Object value; //条目的值
                 Entry(ThreadLocal<?> k, Object v) {
                     super(k);
                     value = v;
                 }
             }
       
     }

构造器

// 构造一个初始包含(firstKey, firstValue)的新映射。
// threadlocalmap是惰性构造的,因此只有在至少有一个条目要放入时才创建一个。 
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化节点容量16
            table = new Entry[INITIAL_CAPACITY];
            // 哈希运算,避免哈希冲突
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 设置值
            table[i] = new Entry(firstKey, firstValue);
            // 大小
            size = 1;
            // 扩容2/3
            setThreshold(INITIAL_CAPACITY);
        }


// 从给定的父映射中构造一个包含所有Inheritable ThreadLocals的新映射。
// 仅由createInheritedMap调用。
private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

getEntry() 方法

 private Entry getEntry(ThreadLocal<?> key) {
             // 计算槽点
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
             // 如果有直接返回
            if (e != null && e.get() == key)
                return e;
            else
                // 找不到,直接返回null
                return getEntryAfterMiss(key, i, e);
        }

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

set()方法

         private void set(ThreadLocal<?> key, Object value) {
                 Entry[] tab = table;
                 int len = tab.length;
     
                  //根据key的HashCode,找到key在数组上的槽点i
                  int i = key.threadLocalHashCode & (len-1);
                 
                 // 从槽点i开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
                 // 若没有现有槽点,则必定有空余槽点,因为没有空间时会扩容 
                 for (Entry e = tab[i];   e != null; 
                                          e = tab[i = nextIndex(i, len)]) {
                     ThreadLocal<?> k = e.get();
                     //找到现有槽点:Key值为ThreadLocal实例
                     if (k == key) {
                         e.value = value;
                         return;
                     }
                     //找到异常槽点:槽点被GC掉,重设Key值和Value值 
                     if (k == null) {
                         replaceStaleEntry(key, value, i);
                         return;
                     }
                 }
                 //没有找到现有的槽点,增加新的Entry
                 tab[i] = new Entry(key, value);
                 //设置ThreadLocal数量
                 int sz = ++size;
     
                 //清理Key为null的无效Entry
                 //没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
                 if (!cleanSomeSlots(i, sz) && sz >= threshold)
                     rehash();
             }

remove()方法

 private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

七 扩展Entry的Key需要使用弱引用?

前提知识

强引用(StrongReference) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 软引用(SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 弱引用(WeakReference) 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

原因

         public void funcA()
         {
             //创建一个线程本地变量
             ThreadLocal local = new ThreadLocal<Integer>(); 
             //设置值
             local.set(100);
             //获取值
             local.get();  
             //函数末尾
         }

一句话:如果是强引用,当方法执行完,实例本该销毁,但是迟迟不被GC清除没会造成严重的内存泄漏

使用总结

         //推荐使用static final线程本地变量
    private static final ThreadLocal<Foo> FOO_THREAD_LOCAL =      new ThreadLocal<Foo>();

	
	 // 删除当前本地线程变量绑定的值
     FOO_THREAD_LOCAL.remove();
  • 由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。
  • 为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更。
  • 使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。