JUC并发编程——ThreadLocal

目录

一、什么是ThreadLocal

二、ThreadLocal的使用

三、ThreadLocal源码分析

四、ThreadLocal导致内存泄漏


一、什么是ThreadLocal

ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量"的时候,线程操作自己的变量副本,从而规避了线程安全问题,是一种空间换时间的思想。

二、ThreadLocal的使用

public class ThreadLocalDemo {
//    private  ThreadLocal threadLocal= ThreadLocal.withInitial(()->{
//        return "test threadlocal"; //在定义ThreadLocal的时候设置一个获取初始值的回调函数。
//    });

    private  static ThreadLocal threadLocal=new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        Thread thread = new Thread(() -> {
            threadLocal.set("test");
            System.out.println(threadLocal.get());
        }, "t1");
        thread.start();
        thread.join();
        String s = threadLocal.get();
        System.out.println("主线程获取不到在thread线程设置的值:"+s);
    }
}

 使用场景

  1. 线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对其他线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。例如用户会话信息,数据库连接,Session数据管理。
  2. 跨函数传递数据,在同一个线程跨类、跨方法传递数据时,如果不用ThreadLocal,就需要靠方法的返回值或者参数传参,这样就增加了代码的耦合度。而ThreadLocal在当前线程设置值以后,在此线程中任何地方都能直接获取到值。避免通过参数传递数据降低代码耦合度。例如spring的编程式事务。

三、ThreadLocal源码分析

set()方法解读

public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            //value被绑定到threadLocal实例
            map.set(this, value);
        } else {
            //没有ThreadLocalMap则创建一个ThreadLocalMap实例,关联到thread实例
            createMap(t, value);
        }
    }

 小结:set()步骤

  • 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
  • 如果map不为空,就将value设置到map中,当前的ThreadLocal作为key。
  • 如果map为空,为该线程创建map,然后设置第一个“key-value对”,key为当前ThreadLocal的实例,value为set()方法参数的值。

get()方法解读

 public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果不为空,以threadlocal实例为key获取值
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //值不为空 返回值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果为空 初始化一个值
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);//绑定值
        } else {
            createMap(t, value);//创建threallocalmap
        }
        if (this instanceof TerminatingThreadLocal) {
            //当线程终止并且已在终止线程中初始化时(即使已使用空值初始化)会收到通知。
            TerminatingThreadLocal.register((TerminatingThreadLocal) this);
        }
        return value;
    }

小结:get()大致步骤

  • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
  • 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象;
  • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个对象, 并且添加到当前线程的threadLocals Map中,并返回
  • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个对象并添加到ThreadLocalMap对象中并返回。

ThreadLocal实现线程隔离的原理其实就是用了Map的数据结构给当前线程缓存了变量的值, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程。当然在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了。

remove()方法解读 

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

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
  private void remove(ThreadLocal key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length; //entry的长度
        int i = key.threadLocalHashCode & (len-1);//key在数组上的槽点
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.refersTo(key)) { 
                //如果e等于key值就删除该值,这是一个native方法,方便垃圾回收
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }

ThreadLocalMap解读 

ThreadLocalMap内部静态类Entry实现
内部使用table数组存储Entry,默认大小INITIAL_CAPACITY(16)

参数说明:
size:table中元素的数量。
threshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素,
如果删除后size >= threshold*3/4时,需要对table进行扩容。
 
static class Entry extends WeakReference> {
    /** The value associated with this ThreadLocal. */
      Object value;

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

可以看到

  • 它并没有实现Map接口;
  • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类;
  • ThreadLocalMap的Entry实现继承了WeakReference>
  • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry。ThreadLocalMap 并不是我们所说的传统的map结构。

Entry对ThreadLocal使用了弱引用(WeakReference,弱引用指向的对象只能生存到下一次垃圾回收之前,也就是说当发生GC回收时,不管内存够不够弱引用的对象都会被回收)。

为什么要使用弱引用呢?如下代码

    public  void  a (){
        ThreadLocal threadLocal = new ThreadLocal<>();
        threadLocal.set("test");//设置值
        threadLocal.get();//获取值
        //结束
    }

JUC并发编程——ThreadLocal_第1张图片

线程执行方法a()时,会创建一个ThreadLocal实例,这个是强引用,在调用set("test")之后,ThreadLocalMap会新建一个Entry实例,这个key是以弱引用的方式指向ThreadLocal的,执行完方法之后,栈帧被销毁,强引用的值也就没有了,但是ThreadLocal的实例还有Entry对应的引用,如果是强引用那么ThreadLocal和value值都不能会GC回收。从而会导致内存泄露问题。 

set()方法解读

  private void set(ThreadLocal key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);

        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.refersTo(key)) {
//如果key等于条目的值,那就直接替换掉旧值
                e.value = value;
                return;
            }

            if (e.refersTo(null)) {
//如果不相等,调用replaceStaleEntry方法创建新值
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 >= 阈值的时候对当前的Table进行重新哈希 
            rehash();
    }

getEntry()方法解读

    private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))
//如果相等直接返回值
                return e;
            else
//如果没有匹配的值,就去后面的条目中查找
                return getEntryAfterMiss(key, i, e);
        }

四、ThreadLocal导致内存泄漏

内存泄漏就是不再使用的内存不能得到回收,引发的最终结果是内存溢出。

在什么情况下ThreadLocal会引发内存泄漏呢?

1、如果一个线程长时间运行而不被销毁,比如线程池。因为对于线程池里面不会销毁的线程, 里面总会存在着threadlocal的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的value对象也不会释放, 就造成了内存泄露。

2、ThreadLocal应用被设置为null之后,且后续在同一Thread实例执行期间,没有发生对其他threadLocal实例的get(),set(),remove()操作(ThreadLocalMap 在执行这些方法时会清空key为null的Entry)。

如何避免ThreadLocal导致的内存泄漏?

1、尽量使用private static final修饰ThreadLocal实例,使用private和final主要是尽可能不让其他的类修改ThreadLocal的引用,static保证ThreadLocal实例的全局唯一。

2、ThreadLocal 使用完之后务必调用remove()方法。

参考

《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著

Java 并发 - ThreadLocal详解 | Java 全栈知识体系

你可能感兴趣的:(JUC,源码分析,JUC,并发编程,ThreadLocal)