3、ThreadLocal详解

原文:原文出处

  ThreadLocal线程局部变量线程本地存储,是一种多线程间并发访问变量的解决方案。与synchoronized与Lock等枷锁的方式不同,ThreadLocal完全不提供锁,而是使用空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是数据共享的解决方案。在高并发量或者锁竞争激烈的情况下,使用ThreadLocal可以在一定程度上减少锁的竞争,减少CPU开销。
  举个栗子:在一个简单的单体服务架构中,用户通过浏览器访问web服务器,登陆之后从服务器获取用户信息,然后会把用户信息使用cookie等方式保存到浏览器本地。我们可以把web服务器可以看成初始化ThreadLocal数据的那个线程,而多个浏览器看成其他的多个线程。当多个浏览器想要获取同一用户信息的时候,需要从唯一的服务器中获取到初始化的用户数据,获取到之后会保存到浏览器本地,当浏览器端如何修改cookie中的用户信息都不会影响到服务器以及其他浏览器的用户数据,这就相当于保证了用户信息的并发访问。

一、ThreadLocal的基本使用

ThreadLocal的常用方法如下
  • public void set(T value){…}:设置当前线程的线程局部变量的值。
  • public Object get(){…}:该方法返回当前线程所对应的线程局部变量。
  • public void remove(){…}:将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected String initialValue() {…}:返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  • public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier){…}:使用Lambda表达式方式初始化ThreadLocal的值,本质上也是调用initialValue方法,返回该线程局部变量初始化值。
1. 初始化ThreadLocal值:

  在创建ThreadLocal的时候,可以通过实现initialValue()方法或者调用withInitial()方法对其值进行初始化。

/**
 * @Author Fluffy Catkin
 * @Date 2020/12/18 23:44
 * @Version 1.0
 * @Description 初始化ThreadLocal
 */
public class InitThreadLocal {
    public static void main(String[] args) throws InterruptedException {
        //方式一 重写initialValue(方法)
        ThreadLocal<String> threadLocal1 = new ThreadLocal<String>(){
            @Override
            protected String initialValue() {
                return "way1:重写initialValue(方法)";
            }
        };
        //方式二 Lambda表达式
        ThreadLocal<String> threadLocal2 =
                ThreadLocal.withInitial(() -> "way2:Lambda 使用withInitial()方法 ");

        //打印
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
    }

}


运行结果:
image.png

2. 多线程修改数据

  当主线程初始化一个ThreadLocal之后,其他线程也可以拿到主线程初始化的数据值,但是在其他线程中对拿到的数据进行修改并不会影响到主线程以及其他线程的数据,即多个线程所获取到的数据是相互独立的。

/**
 * @Author Fluffy Catkin
 * @Date 2020/12/18 23:44
 * @Version 1.0
 * @Description 初始化ThreadLocal
 */
public class ThreadLocalTest {
    public static void main(String[] args) throws InterruptedException {
        //在主线程里创建一个threadLocal
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
        System.out.println(Thread.currentThread().getName()+":初始值==>"+threadLocal.get());
            new Thread(()->{
                //在这个新的线程中可以拿到初始化的数据
                System.out.println(Thread.currentThread().getName()+":新的线程是否可以拿到==>"+threadLocal.get());
                //在这个新的线程中修改他的值
                threadLocal.set(2);
                //修改后,在这个线程中可以拿到修改后的值
                System.out.println(Thread.currentThread().getName()+":新的线程修改后的值==>"+threadLocal.get());
            },"myThread").start();

        Thread.sleep(1000);
        //其他线程中修改修改后,原线程中得值并没有被改变
        System.out.println(Thread.currentThread().getName()+":其他线程中修改后,主线程中的值还是初始值为==>"+threadLocal.get());

        //在主线程中修改值
        threadLocal.set(3);
        //在主线程中看到的值发生了变化
        System.out.println(Thread.currentThread().getName()+":主线程中修改后,主线程中的值发生了改变==>"+threadLocal.get());
    }
}

运行结果:
image.png

二、ThreadLocal源码分析

  • ThreadLocalMap
      文章最开始我们就提到过,ThreadLocal是相当于每一个线程的缓存,多个线程之间的数据相互独立互不影响,那么ThreadLocal是怎么实现的呢?首先我们来看一个ThreadLocal的静态内部类ThreadLocalMap
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
	//类似于Map的key value存储结构,不同的是,ThreadLocalMap的key
	//必须是ThreadLocal对象,value值是ThreadLocalMap 所持有的数据
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
	//初始化长度为16
	private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
	//Entry数组 长度总是2的整数次幂
        private Entry[] table;	
...........

}
  • Thread
      然后看看Thread类,我们可以看到每个线程中持有一个ThreadLocalMap对象的变量threadLocals,用来保存ThreadLocal对应的数据。
public
class Thread implements Runnable {
	...
     /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
     //通过key value的形式保存每个ThreadLocal的数据
     ThreadLocal.ThreadLocalMap threadLocals = null;
	...
}

  这里我们应该清楚了,每个线程都使用ThreadLocalMap 类型的变量threadLocals保存了所有ThreadLOcal的数据,以保证每一个线程数据的隔离性。

  • 初始化方法解析
      ThreadLocal的初始化方式有两个,第一个是重写initialValue方法,第二个通过withInitial方法。本质上都是直接用方法返回用户设置的值:
    protected T initialValue() {
        return null;
    }	

  下面就是通过lambda方式初始化,返回一个继承ThreadLocal的对象SuppliedThreadLocal,并重写其初始化方法initialValue,只是增加了lambda表达式,使用户使用起来更方便:

  public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }


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();
        }
    }
  • get()方法解析
      当执行get()方法时会先获取到当前线程,然后获取到当前线程的ThreadLocal缓存变量threadLocals,如果不为空,并且以当前threadLocalkey可以获取到值,则直接返回;如果为空会为当前线程初始化一个threadLocals变量,并把当前threadLocal对象的值以当前对象threadLocalkey,值为value放入缓存变量threadLocals中。

    public T get() {
	//获取当前线程
        Thread t = Thread.currentThread();
	//获取当前线程中的变量threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {
	    //以当前对象为key获取对应的Entry值
            ThreadLocalMap.Entry e = map.getEntry(this);
	    //如果有值,直接返回获取到的值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
	//如果没有值则调用当前threadLocal对象的初始化方法initialValue()
	//获取到初始化的对象
        return setInitialValue();
    }
...
    //以当前对象threadlocal为key,threadlocal的值为value,
    //放入当前线程的threadLocals中
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
	//如果已经初始化,直接将key、value放入
        if (map != null)
            map.set(this, value);
        else
	//如果没有初始化则创建,并把key、value放入
            createMap(t, value);
        return value;
    }
...
    //返回线程的threadLocals变量
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • set()方法解析
      set()方法与get()方法中调用的setInitialValue()过程类似,唯一不同的是:setInitialValue()方法存入的value是当前threadLocal对象中的初始化值,而set()方法存入用户指定的值。
    public void set(T value) {
	//获取当前线程
        Thread t = Thread.currentThread();
	//获取当前线程中的threadLocals变量
        ThreadLocalMap map = getMap(t);
	//如果不为空,直接将值放入
        if (map != null)
            map.set(this, value);
        else
	//如果为空创建一个对象,并将值放入
            createMap(t, value);
    }
  • remove()方法解析
      remove()方法会将当前线程threadLocals变量以当前threadLocal为key的值移除,需要注意的是,在remove()的时候,会调用expungeStaleEntry(i)方法,将所有key为null的值清理掉,防止造成内存泄漏。因此,在使用ThreadLocal完时候,不要忘了调用remove()方法清理数据
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }


        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) {
		     //将entry的key置为null
                    e.clear();
		    //将该entry的value也置为null
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

三、ThreadLocal内存泄漏分析

  上面我们说过,在使用完threadLocal之后要使用remove()方法将资源释放,否则可能会造成内存泄漏,下面来分析为什么会造成内存泄漏。

1、什么是内存泄漏?

  所谓的内存泄漏是指有无用的对象一直占在内存中,从而导致这块内存一直无法使用,造成内存浪费的现象。
  在java中,运行时数据区的内存都是由JVM管理,堆内存都是通过垃圾回收器进行自动回收,无需开发人员手动释放。JVM是通过可达性分析去标识对象是否是垃圾,然后将垃圾进行回收。而往往开发人员会因为代码的问题造成一些没用的对象仍然可达,这就造成JVM认为这些没用的对象不是垃圾,就不会对其内存进行回收而造成内存一直被占用,从而造成内存泄漏。

2、了解四种引用
  • 强引用:我们遇到最多的引用,常见的就是通过new关键字而生成的对象,比如"Object obj = new Object()"所生成的对象就是强引用,所有的强引用对象永远都不会别垃圾回收器回收
  • 软引用:用来描述一些还有用但是非必须的对象。JDK提供SoftReference类来实现软引用,对于软引用关联的对象,在系统即将发生内存溢出异常之前就会被回收
  • 弱引用:同样用来描述非必须对象,比软引用更弱一些。JDK提供WeakReference类来实现弱引用,当发生垃圾回收的时候,弱引用的对象一定会被垃圾回收器回收
  • 虚引用基本用不到,它也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。JDK提供了PhantomReference类来实现虚引用。
3、OOM演示

  代码如下:


import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Author Fluffy Catkin
 * @Date 2020/12/22 0:06
 * @Version 1.0
 * @Description 测试ThreadLocal内存泄漏
 */
public class ThreadLocalOOM {

    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,30,
            TimeUnit.SECONDS,new LinkedBlockingQueue<>());
    private static final int LOOP_TIMES = 100;

    private static final ThreadLocal<BigParam> threadLocal =
            ThreadLocal.withInitial(BigParam::new);


    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < LOOP_TIMES; i++) {
            int finalI = i;
            executor.execute(
              () -> {
                threadLocal.get();
                System.out.println("add thread ==="+ finalI);
//                threadLocal.remove();
                try {
                  Thread.sleep(1000);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
              });
        }
        Thread.sleep(20000);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(simpleDateFormat.format(new Date())+"gc..................");
        System.gc();
//        Thread.sleep(2000);
        //销毁线程
//        executor.shutdown();
//        System.out.println(simpleDateFormat.format(new Date())+"销毁线程后 gc ..................");
//        System.gc();
//        Thread.sleep(20000);

    }

    static class BigParam{
        private final byte[] a = new byte[1024*1024*50];
    }

}

  在执行前,先设置JVM参数,方便对比:

-XX:+PrintGC -Xms330m -Xmx330m
  • 将代码中的 threadLocal.remove(); 这一行注释后,运行,控制台打印如下:
    1withoutRemoveC
      观察堆内存情况:
    1withoutRemoveP
      可见,在所有线程执行完毕后,在时间2020-12-22 15:15:10左右进行了一次GC,然而内存中始终存在大概250M的内存无法被回收,发生了内存泄漏。
  • 将代码中的 threadLocal.remove(); 这一行的注释放开,运行,控制台打印如下:
    2withoutRemoveC
      观察堆内存情况:
    2withoutRemoveP
      在所有线程执行完毕后,在时间2020-12-22 15:16:34左右进行了一次GC,内存和没有调用remove()方法相比,成功的回收了250m左右的内存,并未发生内存泄漏。
4、原因分析

  在前面源码分析中说到过,每个线程Thread都持有一个ThreadLocalMap类型的对象threadLocals,ThreadLocalMap中的键值对结构Entry继承了WeakReference,它的key是一个被WeakReference弱引用包住的ThreadLocal对象,我们知道,弱引用的对象在发生GC的时候一定会被回收掉,当发生GC的时候,key被回收掉,然而key所对应的value仍然是有值的,他的引用始终被这个线程所持有,如果这个线程不销,那么对于这个value来说根是永远可达的,因此不会垃圾回收器回收,从而产生内存泄漏。

  • 首先看一下ThreadLocalMap中的节点Entry
 /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
	    //key是被WeakReference包住的ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  • remove()方法:
      当执行ThreadLoal的remove()的方法时,会调用ThreadLocalMap的remove()方法,在这个方法中,会去遍历ThreadLocalMap中的每个节点,并调用expungeStaleEntry(i);方法,将key为null的value引用也置为null,这样在GC的时候,就会把无用的value对象进行回收。
/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
	     //调用ThreadLocalMap的remove方法
             m.remove(this);
     }

  继续往下看:

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();
		   //查找所有key为null的节点,把其value也置为null
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
  • 继续看expungeStaleEntry()方法:
 /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
	    //将当前节点的value置为null
            tab[staleSlot].value = null;
	     //将当前节点置为null
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
	    //再遍历一次所有节点,因为别的地方也调用了这个方法
	    //但是其他方法没有循环调用,所以内部再遍历一遍
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //如果key为空则把对应的value置为空,并且把节点置空
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
		    //重新排列数组
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
  • 当线程被销毁时,泄漏的内存得到释放:
      加上下面这段代码(放开注释就行),销毁线程池中的线程,然后再进行GC:
        Thread.sleep(2000);
//        销毁线程
        executor.shutdown();
        System.out.println(simpleDateFormat.format(new Date())+"销毁线程后 gc ..................");
        System.gc();
        Thread.sleep(20000);

  执行后,控制台打印:
图片.png
  内存情况:
图片.png
  可以看到,在2020-12-25 12:30:54左右进行了一次GC,此时线程并未销毁,仍有250m内存无法回收,在2020-12-25 12:30:56左右进行了最后一次GC,此时已经通过executor.shutdown();方法销毁了线程,泄漏的内存成功被回收。OK,对于ThreadLocal的介绍就到这里。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值