JAVA多线程基础篇-类ThreadLocal

1.概述

ThreadLocal类是JDK 1.2中提供的一个类,它是解决多线程并发程序的一个类。它能够提供线程内的局部变量,使得不同线程之间变量不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数组件之间一些公共变量传递的复杂度。在大部分场景下,使用ThreadLocal比直接使用synchronized解决线程安全问题更简单和方便,本文将探索ThreadLocal的具体使用方式和底层原理。

2.ThreadLocal分析

2.1 ThreadLocal基本使用

2.1.1 ThreadLocal基本方法

方法解释
ThreadLocal()创建ThreadLocal对象
set(T value)设置当前线程绑定的局部变量
get()获取当前线程绑定的局部变量
remove()移除当前线程绑定的局部变量

2.1.2 基本使用方式

1.创建ThreadLocal对象

private ThreadLocal<Integer> initInt = new ThreadLocal<>();

由于ThreadLocal是一个泛型类,可以存储不同类型的数据,这里制定了initInt的类型为整数类型。

2.设置变量的值

initInt.set(10);

上述代码设置变量的值为10。

3.获取变量的值

Integer value = initInt.get();

上述代码通过调用get方法获取当前线程在ThreadLocal中的值。

2.1.3 ThreadLocal与synchronized的区别

关键字原理使用场景
synchronized同步机制采用“以时间换空间”的方式,只提供一份变量,让线程排队访问多个线程之间访问资源的同步
ThreadLocal同步机制使用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰多个线程之间实现数据隔离

2.2 ThreadLocal原理

2.2.1 ThreadLocal设计

1.早期设计
在jdk1.2时,ThreadLocal的设计方式为:每个ThreadLocal都会创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离。具体如下图所示:
在这里插入图片描述

2.现在设计
JDK 1.8中的设计思路如下:
(1)每个Thread线程内部都有一个Map(ThradLocalMap);
(2)Map内部存储ThreadLocal对象(key)和线程的变量副本(value);
(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值;
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
主要的实现方法是每个Thread维护了一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值object。具体如下图所示:
在这里插入图片描述

3.改良之后的优势
1.每个Map存储的Entry数量变少,节省存储空间,hash冲突概率变小;
2.当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用。

2.2.2 ThreadLocal源码解析

1.set(T value)

  public void set(T value) {
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程关联的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //判断当前线程关联的ThreadLocalMap是否为空
        if (map != null)
        //若不为空,则调用map.set方法设置Entry的键值
            map.set(this, value);
        else
        //若为空,表明当前线程不存在ThreadLocalMap对象,调用createMap方法进行ThreadLocalMap初始化,并将当前线程t和value作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

   //获取指定线程的ThreadLocalMap对象
  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
   //创建当前线程对应维护的ThreadLocalMap
   void createMap(Thread t, T firstValue) {
        // this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

2.get()

    public T get() {
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程关联的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //判断当前线程关联的ThreadLocalMap是否为空
        if (map != null) {
        	//若不为空,获取当前线程对应的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果当前线程对应的Entry对象不为空,获取Entry对象中的value
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 返回Entry对象中的value值
                return result;
            }
        }
        //返回初始化默认值
        //代码执行到此有两种情况:1.map不存在,线程没有维护ThreadLocalMap对象  2.map存在,但是没有与当前ThreadLocal关联的Entry
        return setInitialValue();
    }

     private T setInitialValue() {
        //调用initialValue方法获取初始化的值,此方法可以被子类重写,不重写默认返回null
        T value = initialValue();
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程关联的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

get方法的大致流程如下:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回默认值。

3.remove()

//删除当前线程中保存的ThreadLocal对应的实体Entry
 public void remove() {
         //获取当前线程对象对应的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
         //判断map是否存在
         if (m != null)
         //若存在,则调用map.remove()方法,删除以当前ThreadLocal为key对应的实体Entry
             m.remove(this);
     }

4.initialValue()

 protected T initialValue() {
        return null;
    }

这个方法的作用是返回该线程局部变量的初始值。
(1)这个方法是一个延迟调用方法,由源代码可知,该方法仅在set方法还未调用,而先调用了get方法时才执行;
(2)该方法缺省直接返回一个null;
(3)若自定义返回null之外的值,可以重新此方法,定义子类继承重写。

2.2.3 ThreadLocalMap源码分析

由源码可知,ThreadLocal的实际操作都是围绕ThreadLocalMap展开的,下面分析ThreadLocalMap,类图如下:
在这里插入图片描述
由上述类图可知,ThreadLocalMap的内部类Entry继承自类WeakReference,是一个弱引用对象。这里涉及到JAVA引用类型的知识,具体可见2.2.5小节。
1.ThreadLocalMap属性

        /**
         * 初始容量-必须是2的整数次幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的table,Entry数组,数组的长度必须是2的整数次幂
         */
        private Entry[] table;

        /**
         *数组中entry的个数,可以用来判断table当前使用量是否超过阈值
         */
        private int size = 0;

        /**
         * 进行扩容的阈值,表内数据大于该值时需要进行扩容操作
         */
        private int threshold; // Default to 0

跟HashMap类似,INITIAL_CAPACITY 代表这个Map的初始容量;table是一个Entry类型的数组,用于存储数据;size代表table数组中Entry对象的数量;threshold代表需要扩容时对应size的阈值。
2.存储结构-Entry

/**
 *Entry继承WeakReference,并且用ThreadLocal作为key
 *如果key为null(entry.get() == null),意味着key不再被引用
 *因此这时候entry也可以从table中清楚
 */
 static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

在ThreadLocalMap中,主要使用Entry来保存数据,不过Entry中的key只能是ThreadLocal对象。由上述代码可知,Entry继承WeakReference,因此key是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。但是value仍是强引用,意味着只有当Thread被回收时,这样value才有被回收的机会,否则,只要线程不退出,value总是存在一个强引用。但是要求所有线程都退出,相对来说较为苛刻,尤其是对线程池来说,大部分线程其实是一直存活在系统的整个生命周期内,可能会造成内存泄漏的危险。
3.构造方法ThreadLocalMap(ThreadLocal<?>firstKey,Object firstValue)

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化table
            table = new Entry[INITIAL_CAPACITY];
            //计算索引
            //& (INITIAL_CAPACITY - 1)采用了hashCode & (size-1)的算法,相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,要求size必须是2的整数次幂,这能保证索引不越界的前提下,使得hash发生冲突的次数减少
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //设置值
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置阈值
            setThreshold(INITIAL_CAPACITY);
        }

   //计算索引部分代码
   private final int threadLocalHashCode = nextHashCode();
   //AtomicInteger类提供了一个线程安全的Integer类,适合高并发场景下使用
   private static AtomicInteger nextHashCode = new AtomicInteger();
   //制定16进制的hash值
   private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

构造函数首先创建了一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。
在计算索引部分的代码中,定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,这个值跟斐波那数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里(Entry[] table中),这样做可以尽量避免hash冲突。
4.set()方法

            private void set(ThreadLocal<?> key, Object value) {
            //
            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)]) {
                ThreadLocal<?> k = e.get();
            //ThreadLocal对应的key存在,直接覆盖之前的值
                if (k == key) {
                    e.value = value;
                    return;
                }
             //key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了
		     //当前数组中的Entry是一个陈旧(stale)的元素
                if (k == null) {
                    //用新元素替换陈旧的元素,该方法进行了垃圾清理,防止内存溢出
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
            * 调用cleanSomeSlots()进行过期元素的清理,结果为boolean类型
            * 若未清理掉任何数据且size超过阈值threshold(len*2/3),则会rehash(),rehash()会先进行线程探测法进行元素清理,若size >= threshold - threshold / 4,则触发扩容操作
            */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

5.remove()方法

       private void remove(ThreadLocal<?> key) {
            //获取table表
            Entry[] tab = table;
            //计算表内数据entry对象个数
            int len = tab.length;
            //根据key计算出hash code
            int i = key.threadLocalHashCode & (len-1);
            //循环遍历table表,若Entry的key与传入key相等,则清除
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //解除引用关系
                    e.clear();
                    //线性探测清理,将遍历到的过期数据的Entry设为null,沿途碰到未过期的数据则将其rehash后重新在table中定位,如果定位的位置有数据,则依次往后遍历找到第一个Entry==null的位置存入,依次往后检查过期数据
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

2.2.4 JAVA引用类型

JAVA语言中有值类型和引用类型,引用类型一般针对对象,JAVA为引用类型专门定制了一个父类叫Reference。WeakReference继承Reference,属于弱引用类型。JAVA中的四种引用类型分别是:强引用、软引用、弱引用和虚引用。

1.强引用(Strong Reference):JAVA中的默认引用就是强引用,任何一个对象的赋值操作就产生了这个对象的强引用。
2.软引用(Soft Reference):软引用的默认类型是SoftReference,在内存不足的情况下,被引用的对象才会被回收。通过调用SoftReference
的get方法来获取该对象,若这个对象没有被GC回收,则返回此对象,否则返回null。
3.弱引用(Weak Reference):弱引用与软引用相似,不同的是弱引用引用的对象只要垃圾回收执行,就会被回收,不管内存是否不足。
4.虚引用(PhantomReference):虚引用的作用是跟踪垃圾回收器收集对象的活动,在GC的过程中,如果发现有PhantomReference,GC则会将引用放到ReferenceQueue中,由程序员自己处理,当程序员调用ReferenceQueue.pull()方法,将引用出ReferenceQueue移除之后,Reference对象会变成Inactive状态,意味着被引用的对象可以被回收了。

2.3 ThreadLocal中内存泄漏问题

2.3.1 内存泄漏概念

1.内存溢出(Memory overflow):没有足够的内存来给申请者使用。
2.内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成内存系统的浪费,导致程序运行速度减慢甚至系统奔溃等严重后果,内存泄漏堆积最终会导致内存溢出。

2.3.2 ThreadLocal内存泄漏的原因

首先看下面的图,表示当前线程使用ThreadLocal堆栈内存之间的关系:

当前线程CurrentThread入栈运行,CurrentThread线程中会使用ThreadLocal,堆内存中ThreadLocal与Entry对象是弱引用,当栈中ThreadLocal引用使用完成时,虚拟机会进行垃圾回收操作,由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadLocal就可以顺利被GC回收,此时Entry中的key==null。但是由于没有手动清除Entry,且当前线程CurrentThread依然在运行的情况下,强引用链:

CurrentThread引用->CurrentThread->threadLocalMap->Entry->Value

存在,value不会被回收,由于没有key,这个value永远不能被访问到,导致内存泄漏。若ThreadLocal与Entry之间是强引用关系,那么threadLocal也不能被回收,从而导致内存泄漏更加严重。
结论:导致ThreadLocal内存泄漏的根本原因是ThreadLocalMap的生命周期与Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

2.3.3 如何防止threadLocal内存泄漏

在不需要ThreadLocal变量时(使用完成时),主动调用remove()进行内存回收。在ThreadLocalMap调用get()、set()、remove()等方法时,都会进行内存回收,负责回收的方法是expungeStaleEntry,上述方法中都直接或间接调用了expungeStaleEntry方法。expungeStaleEntry方法的主要流程如下:
从开始位置向后遍历,将遍历到的过期数据Entry设置为null,沿途碰到未过期的数据则将该数据重新rehash放入table,如果进行hash后的位置有数据,则往后遍历找到第一个Entry==null的位置存入。接着继续往后检查过期数据,直至遇到空的桶才停止循环。

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //传入的staleSlot位置上的数据一定是过期数据,所以将该位置上的数据清空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // for循环向后遍历,直至遇到Entry == null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //若当前Entry的key为null,则将该位置置空
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //若当前遍历的key不为null,将其rehash
                    int h = k.threadLocalHashCode & (len - 1);
                    //若rehash后地址与当前地址不等,则将原本位置Entry置空,再将key的Entry放入rehash后的位置以及其后面位置的第一个为null的位置
                    if (h != i) {
                        tab[i] = null;
                        //此处是rehash后位置不为空,寻找该位置后第一个位置为null
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //返回i,也就是线性探测清理向后遍历遇到的第一个为null的位置
            return i;
        }

2.3.4 InheritableThreadLocal

在实际开发过程中,可能会遇到以下场景:主线程开启一个子线程,但是子线程需要访问主线程中的ThreadLocal对象,涉及到父子线程之间的数据传递。案例代码如下:

public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        for (int i = 0; i < 5; i++) {
            threadLocal.set(i);
            //这里添加一个子线程,希望可以访问上面的threadLocal
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
            }).start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果如下:
在这里插入图片描述
出现这种结果的原因是因为子线程中没有threadLocal,如果希望子线程能够访问父线程的ThreadLocal,可以使用InheritableThreadLocal,让子线程继承父线程的ThreadLocal。将上述代码threadLocal初始化修改为:

InheritableThreadLocal threadLocal = new InheritableThreadLocal();

运行结果如下:
在这里插入图片描述
此时子线程已经能够访问父线程中ThreadLocal对象,它的原理子线程在父线程中是通过new Thread() 方式创建,在Thread的构造方法中有一个init方法,在init方法中会将父线程数据复制到子线程,具体代码如下:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这里需要注意的是,如果不是通过new Thread()方式来创建线程(使用线程池或其它方式),子线程也无法访问父线程的threadLocal对象。threadLocal中变量的赋值就是从主线程的map复制到子线程,它们的value是同一个值,如果对象本身不是线程安全的,那么就会存在线程安全问题。如果子线程在取得值的同时,主线程将InheritableThreadLocals 中的值进行修改,那么子线程取到的值还是旧值。

3.小结

1.ThreadLocal是多线程开发中一个重要的类,但是可能存在内存泄漏问题;
2.解决ThreadLocal使用过程中内存泄漏的主要方法是:使用完ThreadLocal对象后,调用remove方法进行内存释放;
3.弱引用能够在一定程度上帮助解决threadLocal内存泄漏问题。

4.参考文献

1.https://www.cnblogs.com/flydean/p/java-reference-referencetype.html
2.https://juejin.cn/post/6959333602748268575
3.https://www.bilibili.com/video/BV1N741127FH

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值