Java 超详细讲解ThreadLocal类的使用


Posted in Java/Android onApril 07, 2022

Threadlocal有什么用:

简单的说就是,一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值)。如下图:

Java 超详细讲解ThreadLocal类的使用

ThreadLocal使用实例

API介绍

在使用Threadlocal之前我们先看以下它的API:

Java 超详细讲解ThreadLocal类的使用

ThreadLocal类的API非常的简单,在这里比较重要的就是get()、set()、remove(),set用于赋值操作,get用于获取变量的值,remove就是删除当前变量的值.需要注意的是initialValue方法会在第一次调用时被触发,用于初始化当前变量值,默认情况下initialValue返回的是null。

ThreadLocal的使用

说完了ThreadLocal类的API了,那我们就来动手实践一下了,来理解前面的那句话:一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值)

public class ThreadLocalTest {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
	// 重写这个方法,可以修改“线程变量”的初始值,默认是null
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) throws InterruptedException {

        //一号线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("一号线程set前:" + threadLocal.get());
                threadLocal.set(1);
                System.out.println("一号线程set后:" + threadLocal.get());
            }
        }).start();

        //二号线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("二号线程set前:" + threadLocal.get());
                threadLocal.set(2);
                System.out.println("二号线程set后:" + threadLocal.get());

            }
        }).start();

        //主线程睡1s
        Thread.sleep(1000);

        //主线程
        System.out.println("主线程的threadlocal值:" + threadLocal.get());

    }

}

稍微解释一下上面的代码:

每一个ThreadLocal实例就类似于一个变量名,不同的ThreadLocal实例就是不同的变量名,它们内部会存有一个值(暂时这么理解)在后面的描述中所说的“ThreadLocal变量或者是线程变量”代表的就是ThreadLocal类的实例。

在类中创建了一个静态的 “ThreadLocal变量”,在主线程中创建两个线程,在这两个线程中分别设置ThreadLocal变量为1和2。然后等待一号和二号线程执行完毕后,在主线程中查看ThreadLocal变量的值。

程序结果及分析⌛

Java 超详细讲解ThreadLocal类的使用

程序结果重点看的是主线程输出的是0,如果是一个普通变量,在一号线程和二号线程中将普通变量设置为1和2,那么在一二号线程执行完毕后在打印这个变量,输出的值肯定是1或者2(到底输出哪一个由操作系统的线程调度逻辑有关)。但使用ThreadLocal变量通过两个线程赋值后,在主线程程中输出的却是初始值0。在这也就是为什么“一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的”,每个线程都只能看到自己线程的值,这也就是 ThreadLocal的核心作用:实现线程范围的局部变量。

Threadlocal 的源码分析

原理

每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。 这句话刚看可能不是很懂,下面我们一起看完源码就明白了。

前面我们的理解是所有的常量值或者是引用类型的引用都是保存在ThreadLocal实例中的,但实际上不是的,这种说法只是让我们更好的理解ThreadLocal变量这个概念。向ThreadLocal存入一个值,实际上是向当前线程对象中的ThreadLocalMap存入值,ThreadLocalMap我们可以简单的理解成一个Map,而向这个Map存值的key就是ThreadLocal实例本身。

源码

Java 超详细讲解ThreadLocal类的使用

?也就是说,想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到了当前线程中的一个Map中去了,获取ThreadLocal的值时同样也是这个道理。这也就是为什么ThreadLocal可以实现线程之间隔离的原因了。

内部类ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,实现了一套自己的Map结构✨

ThreadLocalMap属性:

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //初始容量16
        private static final int INITIAL_CAPACITY = 16;
        //散列表
        private Entry[] table;
        //entry 有效数量 
        private int size = 0;
        //负载因子
        private int threshold;

ThreadLocalMap设置ThreadLocal 变量

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            
            //与运算  & (len-1) 这就是为什么 要求数组len 要求2的n次幂 
            //因为len减一后最后一个bit是1 与运算计算出来的数值下标 能保证全覆盖 
            //否者数组有效位会减半 
            //如果是hashmap 计算完下标后 会增加链表 或红黑树的查找计算量 
            int i = key.threadLocalHashCode & (len-1);
            
            // 从下标位置开始向后循环搜索  不会死循环  有扩容因子 必定有空余槽点
            for (Entry e = tab[i];   e != null;  e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //一种情况 是当前引用 返回值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //槽点被GC掉 重设状态 
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//槽点为空 设置value
            tab[i] = new Entry(key, value);
            //设置ThreadLocal数量
            int sz = ++size;
			
			//没有可清理的槽点 并且数量大于负载因子 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap属性介绍?:

  • 和普通Hashmap类似存储在一个数组内,但与hashmap使用的拉链法解决散列冲突不同的是 ThreadLocalMap使用开放地址法
  • 数组 初始容量16,负载因子2/3
  • node节点 的key封装了WeakReference 用于回收

ThreadLocalMap存储位置

储存在Thread中,有两个ThreadLocalMap变量

Java 超详细讲解ThreadLocal类的使用

threadLocals 在ThreadLocal对象方法set中去创建 也由ThreadLocal来维护

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

inheritableThreadLocals 和ThreadLocal类似 InheritableThreadLocal重写了createMap方法

void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

inheritableThreadLocals 作用是将ThreadLocalMap传递给子线程

Java 超详细讲解ThreadLocal类的使用

init方法中 条件满足后直接为子线程创建ThreadLocalMap

Java 超详细讲解ThreadLocal类的使用

注意:

  • 仅在初始化子线程的时候会传递 中途改变副线程的inheritableThreadLocals 变量 不会将影响结果传递到子线程 。
  • 使用线程池要注意 线程不回收 尽量避免使用父线程的inheritableThreadLocals 导致错误

Key的弱引用问题

为什么要用弱引用,官方是这样回答的

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

生命周期长的线程可以理解为:线程池的核心线程

ThreadLocal在没有外部对象强引用时如Thread,发生GC时弱引用Key会被回收,而Value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

  • key 使用强引用?: 引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用?: 引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

Java8中已经做了一些优化如,在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

java中的四种引用

  • 强引用?: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
  • 软引用?: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
  • 弱引用?: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
  • 虚引用?: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)

通常ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长,如果没有手动删除对应key(线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏),一定会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除,Java8已经做了上面的代码优化。

总结:

ThreadLocal的作用: 实现线程范围内的局部变量,即ThreadLocal在一个线程中是共享的,在不同线程之间是隔离的。

ThreadLocal的原理: ThreadLocal存入值时使用当前ThreadLocal实例作为key(并不是以当前线程对象作为key),存入当前线程对象中的Map中去。最开始在看源码之前,我以为是以当前线程对象作为key将对象存入到ThreadLocal中的Map中去…

到此这篇关于Java 超详细讲解ThreadLocal类的使用的文章就介绍到这了,更多相关Java ThreadLocal内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Java/Android 相关文章推荐
手把手教你用SpringBoot将文件打包成zip存放或导出
Jun 11 Java/Android
java Nio使用NioSocket客户端与服务端交互实现方式
Jun 15 Java/Android
Spring实现内置监听器
Jul 09 Java/Android
使用@Value值注入及配置文件组件扫描
Jul 09 Java/Android
springboot+WebMagic+MyBatis爬虫框架的使用
Aug 07 Java/Android
Java spring单点登录系统
Sep 04 Java/Android
java代码实现空间切割
Jan 18 Java/Android
Java 垃圾回收超详细讲解记忆集和卡表
Apr 08 Java/Android
java中为什么说子类的构造方法默认访问的是父类的无参构造方法
Apr 13 Java/Android
java版 联机五子棋游戏
May 04 Java/Android
JAVA springCloud项目搭建流程
May 11 Java/Android
Java中的Kafka为什么性能这么快及4大核心详析
Sep 23 Java/Android
Java 通过手写分布式雪花SnowFlake生成ID方法详解
Java详细解析==和equals的区别
Apr 07 #Java/Android
Java 超详细讲解hashCode方法
Apr 07 #Java/Android
Java 关于String字符串原理上的问题
Apr 07 #Java/Android
Java虚拟机内存结构及编码实战分享
Java Lambda表达式常用的函数式接口
Apr 07 #Java/Android
Android Rxjava3 使用场景详解
Apr 07 #Java/Android
You might like
thinkphp的CURD和查询方式介绍
2013/12/19 PHP
php+ajax实时输入自动搜索匹配的方法
2014/12/26 PHP
PHP使用redis实现统计缓存mysql压力的方法
2015/11/14 PHP
javascript Array.sort() 跨浏览器下需要考虑的问题
2009/12/07 Javascript
28个JS验证函数收集
2010/03/02 Javascript
JS基础之undefined与null的区别分析
2011/08/08 Javascript
jquery中get,post和ajax方法的使用小结
2014/02/04 Javascript
jquery显示loading图片直到网页加载完成的方法
2015/06/25 Javascript
jquery 实现输入邮箱时自动补全下拉提示功能
2015/10/04 Javascript
学习JavaScript设计模式(多态)
2015/11/25 Javascript
js控住DOM实现发布微博效果
2016/08/30 Javascript
Bootstrap按钮组简单实现代码
2017/03/06 Javascript
详解JavaScript数组过滤相同元素的5种方法
2017/05/23 Javascript
vue实现百度搜索下拉提示功能实例
2017/06/14 Javascript
vue实现分页加载效果
2019/12/24 Javascript
[02:22]《新闻直播间》2017年08月14日
2017/08/15 DOTA
Python 调用DLL操作抄表机
2009/01/12 Python
用Python写冒泡排序代码
2016/04/12 Python
浅谈Python中函数的参数传递
2016/06/21 Python
Python实现快速排序算法及去重的快速排序的简单示例
2016/06/26 Python
python 统计列表中不同元素的数量方法
2018/06/29 Python
Python的bit_length函数来二进制的位数方法
2019/08/27 Python
css3 线性渐变和径向渐变示例附图
2014/04/08 HTML / CSS
使用CSS3和Checkbox实现JQuery的一些效果
2015/08/03 HTML / CSS
韩国三星旗下的一家超市连锁店:Home Plus
2016/07/30 全球购物
荷兰度假屋租赁网站:Aan Zee
2020/02/28 全球购物
长青弘远的面试题
2012/06/09 面试题
项目经理岗位职责
2013/11/11 职场文书
上课打牌的检讨书
2014/02/15 职场文书
体育教师求职信
2014/05/24 职场文书
法制宣传标语
2014/06/23 职场文书
2014年乡镇工会工作总结
2014/12/02 职场文书
2015年班组建设工作总结
2015/05/13 职场文书
青涩记忆观后感
2015/06/18 职场文书
Python合并多张图片成PDF
2021/06/09 Python
如何使用 resize 实现图片切换预览功能
2021/08/23 HTML / CSS