【多线程系列-05】深入理解ThreadLocal的底层原理和基本使用

news2024/11/15 19:47:24

多线程系列整体栏目


内容链接地址
【一】深入理解进程、线程和CPU之间的关系https://blog.csdn.net/zhenghuishengq/article/details/131714191
【二】java创建线程的方式到底有几种?(详解)https://blog.csdn.net/zhenghuishengq/article/details/127968166
【三】深入理解java中线程的生命周期,任务调度https://blog.csdn.net/zhenghuishengq/article/details/131755387
【四】深入理解java中线程间的通信机制https://blog.csdn.net/zhenghuishengq/article/details/132072145
【五】深入理解java中线程间的通信机制https://blog.csdn.net/zhenghuishengq/article/details/132192325

深入理解ThreadLocal的底层原理和基本使用

  • 一,ThreadLocal
    • 1,ThreadLocal简介
    • 2,ThreadLocal的基本使用
    • 3,ThreadLocal的底层源码(重点)
      • 3.1,ThreadLocalMap的底层结构和原理
      • 3.2,set,get,remove方法底层实现
    • 4,Hash冲突解决方式
    • 5,ThreadLocal造成内存泄漏的原因

一,ThreadLocal

1,ThreadLocal简介

在官网中是这样介绍ThreadLocal的:ThreadLocal提供线程局部变量,这些变量与正常的变量不同,每一个线程在访问ThreadLocal实例的时候,都有自己的、独立的初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来

也就是说 ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

因此也可以看出 ThreadLocal 和 Synchonized 都用于解决多线程并发访问。但是 ThreadLocalsynchronized 有着本质的差别,synchronized 是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问,而ThreadLocal 则是副本机制,此时不论多少线程并发访问都是线程安全的。

简而言之就是:假设篮球场上10个人,只有一个篮球,那么这十个人都得抢这一个篮球,并且还要考虑同时抢大打出手的问题,就需要加锁,这无疑是效率太低;而ThreadLocal为了解决这种资源竞争的问题,就引用了副本机制,就是人手一个篮球,每个篮球和一个人一一对应,这样就即提高了效率,也不会出现抢占的问题。用一句话形容synchronized,lock等这些锁:群雄逐鹿起纷争;用一句话形容ThreadLocal就是:人手一份天下安

2,ThreadLocal的基本使用

可以先到官网中先查看其api:https://docs.oracle.com/javase/8/docs/api/index.html

主要有以下的方法,可以说这个类的方法是很少的了,因此想要用的好只需要注意里面的细节即可

在这里插入图片描述

可以通过new 关键字创建一个ThreadLocal,并且重写里面的 initialValue 方法来初始化局部变量的副本的值

ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

也可以直接通过匿名内部类的方式创建一个ThreadLocal,此时可以直接通过调用类方法 withInitial 来初始化局部变量副本的值,开发中更加推荐使用这种方式

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

set,get和remove就比较简单了,直接通过实例调用即可。

threadLocal.set("xxx");
threadLocal.get();
threadLocal.remove();

如下,写一个小demo,在测试类内部创建一个静态的内部类,并继承Thread类

/**
 * @author zhenghuisheng
 * @date : 2023/8/9
 */
public class ThreadLocalTest {
    //初始化threadLocal
    static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
   	//主线程
    public static void main(String[] args) {
        t t = new t(10);
        t.start();
    }
    //静态内部线程类
    static class t extends Thread{
        private Integer i;
        public t(Integer i){
            this.i = i;
        }
        //重写run方法
        @Override
        public void run() {
            System.out.println(i);
            threadLocal.set(i+100);
            System.out.println(threadLocal.get());
            //防止内存泄漏
            threadLocal.remove();
        }
    }
}

3,ThreadLocal的底层源码(重点)

3.1,ThreadLocalMap的底层结构和原理

在threadLocal类中,是一个带有泛型的类,该类中主要有一些初始化,get,set,remove等方法

public class ThreadLocal<T> {...}

由于每个线程中可能会存在多个副本,因此在这个ThreadLocal类内部,又有一个 ThreadLocalMap 静态内部类,主要用于存储这些ThreadLocal副本,由于map结构查询数据的时间复杂度为O(1),因此优先考虑使用map这种数据结构存储数据

static class ThreadLocalMap {...}

既然是用到了map这种数据结构,就要考虑hash冲突的问题,hashMap解决hash碰撞是通过数组加链表再加红黑树实现的(jdk1.8),而这个 ThreadLocalMap里面解决这个hash碰撞是引入了 Entry 数组实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UE1I1MfA-1691571588790)(img/1691560539006.png)]

Entry是一个类,他是 ThreadLocalMap 里面的一个静态内部类

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

从他的构造方法中可以看出,有两个参数,key是这些线程的副本,value就是对应的值

Entry(ThreadLocal<?> k, Object v)

又由于这个 ThreadLocalMap 的构造方法中,会初始化一个最大整型容量的table数组,里面主要存储这个entry对象,因此实现这个ThreadLocalMap底层数据结合主要是通过数组的方式实现

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

而又由于这个ThreadLocal和他的静态内部类ThreadLocalMap都是 Thread 类的成员变量

public class Thread implements Runnable {
    ...
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

因此根据层层关系,可以得知这几个类之间的关系如下图,ThreadLocalMap是thread实例的一个成员变量,创建一个ThreadLocalMap会创建一个Table数组,主要是存放Entry的实例,该实例由键值对组成,key值就是ThreadLocal副本,value值就是存到该副本的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFOgbAdt-1691571588791)(img/1691561883917.png)]

3.2,set,get,remove方法底层实现

分析完底层的存储结构和原理,那么再来分析threadLocal的set,get,remove等方法就很简单了。

首先看set方法的源码,首先会先获取到当前线程,随后通过getMap获取到这个ThreadLocalMap,如果map为空则创建一个map,并且将值加入到map中,不为空则直接将值加入到map中。

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

随后set的方法如下,就是将ThreadLocal副本作为key,需要存储的value作为值,期间会经过一些位运算,来解决hash冲突的问题,最终将生成一个Entry对象,随后将这个对象存储在数组里面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9xfAnR54-1691571588791)(img/1691563080937.png)]

其次再来看看get方法,其实现也很简单,也是先获取到当前线程,然后获取到ThreadLocalMap,随后去ThreadLocalMap里面的数组取值就行

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

在获取这个Entry实例时,也会经过一些位运算来获取值

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
        return getEntryAfterMiss(key, i, e);
}

最后剩下一个remove方法了,也是先获取到这个ThreadLocalMap,随后删除数组里面的值

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

由于ThreadLocalMap底层是由Entry数组实现的,因此主要删除Entry数组里面的值即可,也要做一个hash位运算

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;
        }
    }
}

因此threadLocal的set,get和remove方法讲完了,其实就是对一个数组的操作,期间可能需要处理一些hash冲突问题。

4,Hash冲突解决方式

hash冲突指的是数据在压缩映射的时候冲突,如有10桶水,要将这10桶水倒入到5个桶里面,桶大小一样,那么肯定会装不下,这就是所谓的冲突。

常见解决hash冲突的方法有开放定址法、再Hash法、链地址法。在hashMap中,所采用的就是链地址法,就是说当发生hash冲突之后,就把后加进来的值存放到链表以及红黑树里面。

但是在这个ThreadLocal中,并没有采用这种链地址法,很明显在源码中,只看到了只有一个数组,并没有看到链表红黑树等的出现,而是采用的是开放定址法。开放定址法就是说,如果发生hash冲突,后进来的就往后找空位,如果为空则将值插入进去。

开放定址法实现方式主要有:线性探测再散列、二次探测再散列、伪随机探测再散列,这几种方式区别在于每次定位下一个地址的方式不同。线性是每当发生hash冲突时,往后一步再次判断;二次是每n的平方步判断,如这次验证第一个格子,下次跳2的平方个格子,再下次就是跳3的平方个格子,以此类推…;伪随机就是随机步数判断。

在ThreadLocal中,采用的是线性探测再散列的开放定址法。在set元素时也可以发现,如果出现了hash冲突,就会依次的往下一个元素找。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

5,ThreadLocal造成内存泄漏的原因

在谈这个内存泄漏之前,一定需要有点jvm的基础和常识,可以先查看的jvm系列:https://blog.csdn.net/zhenghuishengq/category_11862872.html ,至少需要知道堆存什么,栈存什么,对象是否能被回收,垃圾回收的方法等等

接下来就举一个简单的例子,就是每次,随后使用 JProfiler 工具打开

public class ThreadLocalTest {

    //初始化threadLocal
    static ThreadLocal<byte[]> threadLocal = ThreadLocal.withInitial(() -> new byte[0]);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new t().start();
            Thread.sleep(500);
        }
        System.out.println(threadLocal.get());
    }

    static class t extends Thread{
        @Override
        public void run() {
            byte[] b = new byte[1024*20];
            threadLocal.set(b);
        }
    }
}

随后查看这个内存的结果,很明显我的内存的使用一直在增加,按理来说我这个对象set进去了,但是get的值却是空的,按理来说虽然逃逸分析可以随着入栈和出栈将不被引用的对象给当做垃圾回收,但是这个对象是存储在entry对象里面的,由于jvm主流的还是使用gc root可达性分析算法来判断对象是否能回收的,因此这里也可以猜测这个entry对象是被引用这的,不可能被回收,而且看内存的增高也知道是没有被回收的,那么为啥get的时候获取到的值为空呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHkLI8u3-1691571588792)(img/1691566915291.png)]

因此又得回到源码里面来找出路,后面才知道是这个Entry这个类的问题,因为这个类继承了 WeakReference这个弱引用类,并且里面有着泛型ThreadLocal

static class Entry extends WeakReference<ThreadLocal<?>>{}

引用分为强引用、软引用、落引用、虚引用,虚引用指的就是无论空间是否存在,下次gc都会被回收。看似这里是被entry引用着这个key和value,但是又与这个ThreadLocal是一个弱引用,也就是说这个entry的key是一个弱引用,因此在run方法出栈的后,下次gc就会将这个key给回收掉,但是value是还存在的,value还存在entry里面,key被回收了,因此在调用get()方法的时候,获取到的值为空,而value没有被回收,因此这个内存一直在增加,并且由于是被强引用着,因此gc不掉,这就是典型的内存泄漏问题,即应该被回收的内容没有被回收。

如下图,在入栈run方法中,由于这个threadLocal是一个变量,因此存储在当前线程的栈帧里面,即被当前线程所引用着,但是当该方法出栈之后,该栈帧就被销毁了,那么就只剩一个key指向这这个threadLocal,而由于threadLocal是一个弱引用,那么在下次gc的时候,threadLocal就直接给回收掉了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POPFCy71-1691571588792)(img/1691568574388.png)]

那么使用这个threadLocal时,就需要在每次使用完后,即时的remove掉,才能避免这种内存泄漏问题

threadLocal.remove();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/853637.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

西安企业通配符https证书订购流程

通配符https证书可以保护主域名以及主域名下多个子域名的安全&#xff0c;例如*.example.com可以保护www.example.com、blog.example.com等所有以example.com结尾的子域名。这样不仅可以节省证书费用&#xff0c;也可以减少证书管理的工作量。 而企业通配符https证书是通配符ht…

实战指南:使用OpenCV 4.0+Python进行机器学习与计算机视觉

&#x1f482; 个人网站:【办公神器】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 1.背景2. 安装和配…

2023河南萌新联赛第(五)场:郑州轻工业大学--买爱心气球

题目链接&#xff1a;A-买爱心气球_2023河南萌新联赛第&#xff08;五&#xff09;场&#xff1a;郑州轻工业大学 (nowcoder.com) 题目描述 Alice 和 Bob 是一对竞技编程选手&#xff0c;他们路过了一家气球店&#xff0c;发现有 m 个大爱心气球和 n 个小爱心气球。他们决定玩…

视频网站如何选择国外服务器?

​ 视频网站如何选择国外服务器? 地理位置&#xff1a;选择靠近目标用户群体的国外服务器位置是至关重要的。若用户主要集中在中国以外的地区&#xff0c;因您应选择位于用户所在地附近的服务商&#xff0c;以确保视频的传输速度。 带宽和速度&#xff1a;选择带宽足够且方便升…

一文详解2023 Smartbi V11系列新品发布会精彩看点

8月8日&#xff0c;2023 Smartbi V11系列新品发布会圆满落幕&#xff0c;在活动上重磅发布了全新升级的Smartbi V11版本&#xff0c;分别是Smartbi 一站式ABI平台&#xff08;Smartbi Insight V11&#xff09;和智慧数据运营平台&#xff08;Smartbi Eagle V11&#xff09;&…

前端下载文件

前端可以通过使用 JavaScript中的 fetch 或者 XMLHttpRequest 来下载文件&#xff1b; 使用fetch进行文件下载&#xff1b; fetch(http://example.com/file.pdf).then(response > response.blob()).then(blob > {// 创建一个临时的URL对象const url window.URL.create…

0101docker mysql8镜像主从复制-运维-mysql

1 概述 主从复制是指将主数据库的DDL和DML操作通过二进制日志传到从库服务器&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而使得从库和主库的数据保持同步。 Mysql支持一台主库同时向多台从库进行复制&#xff0c;从库同时可以…

“精准学习嵌入式开发:明确目标,提升技能“

嵌入式领域涵盖广泛&#xff0c;不可能一次性掌握所有知识。因此&#xff0c;明确学习目标和方向非常重要。选择感兴趣且与职业发展相关的领域进行深入学习是明智之举。 嵌入式技术在不断发展&#xff0c;过去与现在存在差异。选择学习当前行业的主流技术和趋势是明智选择。掌…

【Linux进程篇】进程概念(2)

【Linux进程篇】进程概念&#xff08;2&#xff09; 目录 【Linux进程篇】进程概念&#xff08;2&#xff09;进程状态Linux对进程的说法linux中的信号 进程状态查看Z(zombie)——僵尸进程僵尸进程的危害 孤儿进程 进程优先级基本概念查看系统进程PRI &#xff08;优先级priori…

MyCat概述

1.MyCat概述 MyCat是阿里巴巴的产品&#xff0c;他是开源的、基于Java语言编写的MySQL数据库中间件。可以像使用mysql一样来使用mycat&#xff0c;对于开发人员来说根本感觉不到mycat的存在。 MyCat下载地址&#xff1a;http://dl.mycat.org.cn/ MyCat官网&#xff1a;http:/…

Django入门 - 路由Route的基本使用

文章目录 1. 直接访问视图函数&#xff0c;没有使用子路由2. 使用子路由 urls.py 我们一般叫它根路由 1. 直接访问视图函数&#xff0c;没有使用子路由 MyDjangoPro2\views.py 代码 from django.shortcuts import renderfrom django.http import HttpResponse# 视图函数Views …

Minio使用及整合起步依赖

说明&#xff1a;Minio是开源的对象存储服务器&#xff0c;相当于免费版的OSS&#xff0c;本文介绍在Linux环境下部署Minio服务器&#xff0c;并在SpringBoot中使用&#xff0c;最后将Minio的代码打包成一个起步依赖。 安装&启动 第一步&#xff1a;下载 首先&#xff0…

使用vue-grid-layout时 You may need an appropriate loader to handle this file type.

使用vue-grid-layout时 You may need an appropriate loader to handle this file type. node版本不匹配 我的node v14.16.0 vue-gride-layout 需要用 v 2.3.7的版本 卸载后重新安装即可

诺瓦星云面试汇总

1、C语言向一个内存地址写值&#xff0c; int main() {int value 42;int *ptr (int *)0x12345678; // Replace with the desired memory address*ptr value 2、申请释放内存 申请内存int *dynamicArray (int*)malloc(size *sizeof(int));释放内存 free(dynamicArray)…

哪些国家可以申请访问学者?

根据知识人网的小编了解&#xff0c;许多国家都允许外国学者申请访问并参与学术交流。这些国家提供了丰富多样的研究机会&#xff0c;有助于促进全球学术合作与知识交流。以下是一些允许申请访问学者的国家&#xff1a; 1. 美国&#xff1a;作为全球科研领域的重要一员&#xf…

远景智能PMO负责人严晓婷受邀为第十二届中国PMO大会演讲嘉宾

上海远景科创智能科技有限公司PMO负责人严晓婷女士受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;能源物联网产品标准项目和非标准项目的并行管理。大会将于8月12-13日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&#xff1…

《合成孔径雷达成像算法与实现》Figure3.6

代码复现如下&#xff1a; clc clear all close all%参数设置 TBP 100; %时间带宽积 T 10e-6; %脉冲持续时间%参数计算 B TBP/T; …

【回眸】AurixTC397的MPS8875A开发之展频篇

目录 前言 【回眸】AurixTC397的MPS8875A开发之展频篇 知识储备 看懂芯片手册 调整寄存器写入值 修改写入寄存器代码 修改主函数代码 验证和测量 前言 因公需要&#xff0c;大半年来一直对AurixTC397进行开发。实物板子特别大&#xff0c;有很多很多芯片&#xff0c;具…

2023-08-09 ssh-add id_rsa 提示Permissions 0777 for ‘id_rsa‘ are too open

一、ssh-add id_rsa 提示Permissions 0777 for id_rsa are too open Permissions 0777 for id_rsa are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored.二、意思是说公钥文件权限太宽了&#xff0c;需…

【校招VIP】java语言考点之基本数据类型

考点介绍&#xff1a; 基本数据类型的长度、自动升级、JVM存储和封装类的相关考点&#xff0c;是校招常见考点。基础考点不能出错 一、考点题目 1、JAVA 中的几种基本数据类型是什么&#xff0c;各自占用多少字节解答&#xff1a;先了解2个单词先&#xff1a;1、bit --位&am…