JUC并发编程与源码分析笔记10-聊聊ThreadLocal

news2024/11/19 21:31:38

ThreadLocal简介

恶心的大厂面试题

  • ThreadLocal中ThreadLocalMap的数据结构和关系
  • ThreadLocal的key是弱引用,这是为什么
  • ThreadLocal内存泄漏问题你知道吗
  • ThreadLocal中最后为什么要加remove方法

是什么

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

能干嘛

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过调用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本值,避免线程安全问题。

api介绍

在这里插入图片描述

永远的hello world讲起

import cn.hutool.core.convert.Convert;

import java.util.Random;

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                for (int j = 0; j < size; j++) {
                    house.saleSynchronized();
                }
                System.out.println(Thread.currentThread().getName() + ":" + size);// saleSynchronized()
            }, Convert.toStr(i)).start();
        }
        Thread.sleep(1000);
        System.out.println("总共:" + house.count);
    }
}

class House {
    int count;

    public synchronized void saleSynchronized() {
        count++;
    }
}

ThreadLocal使用完之后,必须自定义回收,特别是在使用线程池的时候,线程经常会被复用,如果不清理自定义的ThreadLocal变量,有可能影响后序逻辑或造成内存泄露,通常在finally中进行回收。
线程池中,未进行ThreadLocal的remove(),下一个线程get的值,就有可能是上一个线程的值,而不是原始值,这就造成了运算错误。

import cn.hutool.core.convert.Convert;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        AtomicInteger sum = new AtomicInteger();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int j = 0; j < size; j++) {
                        house.saleByThreadLocal();
                    }
                    sum.addAndGet(house.threadLocal.get());
                    System.out.println(Thread.currentThread().getName() + ":" + house.threadLocal.get());// saleSynchronized()
                } finally {
                    house.threadLocal.remove();
                }
            }, Convert.toStr(i)).start();
        }
        Thread.sleep(1000);
        System.out.println("总共:" + sum.get());
    }
}

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

    public void saleByThreadLocal() {
        threadLocal.set(threadLocal.get() + 1);
    }
}

添加ThreadLocal的remove()方法,那么从线程池中获取的线程,每次拿到的都是初始值,不会受到上一个线程的影响,此时计算结果正确。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalDemo {
    public static void main(String[] args) {
        Demo demo = new Demo();
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        try {
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    Integer before = demo.threadLocal.get();
                    demo.add();
                    Integer after = demo.threadLocal.get();
                    System.out.println(Thread.currentThread().getName() + "--before=" + before + "--after=" + after);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

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

    public void add() {
        threadLocal.set(1 + threadLocal.get());
    }
}

ThreadLocal源码分析

在Thread.java里,有一个ThreadLocal.ThreadLocalMap属性,而ThreadLocalMap又是ThreadLocal的一个静态内部类。ThreadLocalMap内是一个Entry的静态内部类,它继承自WeakReference。
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map,以ThreadLocal为Key,经过两层包装的ThreadLocal对象。
JVM内部维护了一个线程版的Map<ThreadLocal,Value>,通过ThreadLocal对象的set方法,把ThreadLocal对象自己当做Key,放进了ThreadLocalMap中,每个线程要用到这个T的时候,从当前线程的Map里获取,通过这样让每个线程用用自己的独立的变量,人手一份,竞争条件被彻底消除,在并发模式下绝对安全的变量。

ThreadLocal内存泄露问题

内存泄露:不再被使用的对象或者变量占用的内存不能被回收。
在这里插入图片描述

强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

public class ThreadLocalDemo {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);
        myObject = null;
        System.gc();
        System.out.println("gc after:" + myObject);
    }
}

class MyObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("MyObject.finalize");
    }
}

软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时它会被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

import java.lang.ref.SoftReference;

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("softReference = " + softReference);
        System.gc();
        Thread.sleep(1000);
        System.out.println("内存充足,softReference = " + softReference.get());
        // 配置:VM options为:-Xms10m -Xmx10m
        try {
            byte[] bytes = new byte[20 * 1024 * 1024];// 20M
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("内存不足,softReference = " + softReference.get());
        }
    }
}

class MyObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("MyObject.finalize");
    }
}

软引用的一个场景:有一个应用需要读取大量的本地图片,每次读取会影响硬盘性能,一次性全部加载到内存会造成内存溢出,此时,可以使用一个HashMap来保存图片路径和图片对象的映射关系,内存不足时,自动回收这些缓存图片,避免内存溢出。

Map<String, SoftReference<BitMap>> imageCache = new HashMap<>();

弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

import java.lang.ref.WeakReference;

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("System.gc()前:weakReference = " + weakReference);
        System.gc();
        Thread.sleep(1000);
        System.out.println("System.gc()后:weakReference = " + weakReference.get());
    }
}

class MyObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("MyObject.finalize");
    }
}

虚引用

  1. 虚引用必须和引用队列(ReferenceQueue)联合使用
    虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
  2. PhantomReference的get方法总是返回null
    虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
  3. 处理监控通知使用
    换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.List;

public class ThreadLocalDemo {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);
        // 配置:VM options为:-Xms10m -Xmx10m
        // System.out.println(phantomReference.get());// 总是null
        List<byte[]> list = new ArrayList<>();
        new Thread(() -> {
            while (true) {
                list.add(new byte[1 * 1024 * 1024]);// 1M
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("list add ok\t" + phantomReference.get());
            }
        }, "t1").start();
        new Thread(() -> {
            while (true) {
                Reference<? extends MyObject> reference = referenceQueue.poll();
                if (reference != null) {
                    System.out.println("有虚引用加入了referenceQueue");// 非必现
                    break;
                }
            }
        }, "t2").start();
    }
}

class MyObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("MyObject.finalize");
    }
}

小总结

在这里插入图片描述
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里的ThreadLocalMap内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

  1. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,value是传递进来的对象
  2. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocaMap获取value。正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

为什么ThreadLocalMap的Entry的key要使用弱引用呢

在这里插入图片描述

public class ThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal();
        threadLocal.set("王劭阳");
        System.out.println(threadLocal.get());
    }
}

在这段程序中,观察ThreadLocal对象threadLocal有哪些引用。
在main方法里,有一个threadLocal的强引用,在ThreadLocalMap的key上,有一个threadLocal的弱引用。
假设ThreadLocalMap中的key是强引用,当main线程执行完毕后,main线程已经被回收了,指向threadLocal的引用,也应该被回收,但是因为ThreadLocalMap的key是一个指向threadLocal的强引用,导致threadLocal不能被回收,此时造成内存泄露。
假设ThreadLocalMap中的key是弱引用,当main线程执行完毕后,main线程已经被回收了,指向threadLocal的引用,也应该被回收,此时ThreadLocalMap里的key是弱引用,threadLocal就可以被回收,大概率减少了内存泄露的问题(还有一个key为null的坑,后面会说)。
使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

清除脏Entry

当我们调用get()set()remove()的时候,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
当我们为ThreadLocal变量赋值时,实际就是往当前threadLocalMap的Entry里存放东西,key为threadLocal,value为存放的值,Entry中的key是一个弱引用,当发生GC的时候,就会被回收,导致Entry中存在key为null的引用,就没有办法通过key为null的Entry获取到value,但是这个value依旧占用着内存,所以value永远无法回收,就造成了内存泄漏问题。
虽然弱引用保证了key指向的ThreadLocal对象能被及时回收,但是value指向的对象,需要ThreadLocalMap调用get()set()的时候,才会根据key=null回收整个Entry,弱引用并不能100%保证不发生内存泄露,所以,我们在不使用某个ThreadLocal对象后,要手动调用remove()方法来删除它。
清除脏Entry的方法是:java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry

小总结

  1. 使用ThreadLocal的时候,进行初始化:ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> 初始值);,避免空指针异常
  2. 建议把ThreadLocal修饰为static:ThreadLocal能实现线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以ThreadLocal只初始化一次,只分配一块内存空间就可以了,没必要作为成员变量多次被初始化
  3. 用完记得手动remove()
  • ThreadLocal并不解决线程间数据共享问题
  • ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal通过隐式在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题,通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键为null的Entry对象的值,以及Entry对象本身,防止内存泄漏,属于安全加固方法

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

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

相关文章

基于神经辐射场(Neural Radiance Fileds, NeRF)的三维重建- 简介(1)

Nerf简介 Nerf&#xff08;neural Radiance Fileds&#xff09; 为2020年ICCV上提出的一个基于隐式表达的三维重建方法&#xff0c;使用2D的 Posed Imageds 来生成&#xff08;表达&#xff09;复杂的三维场景。现在越来越多的研究人员开始关注这个潜力巨大的领域&#xff0c;也…

十大排序(C++版)

测试排序的题目&#xff1a; 912. 排序数组 - 力扣&#xff08;LeetCode&#xff09; 堕落的做法&#xff1a; class Solution { public:vector<int> sortArray(vector<int>& nums) {sort(nums.begin(),nums.end());return nums;} };视频推荐&#xff1a; …

洛谷:P1554 梦中的统计 JAVA

思路&#xff1a;定义一个长度为10的数组&#xff0c;数组下标代表数组元素的数字&#xff0c;比如arr[0]代表数字0.用一个for循环&#xff0c;对每个数先取余再取整&#xff0c;知道取整得到的数为0&#xff0c;说明该数字已经被拆解完了。今天又学了一个输入&#xff0c;原来…

2020蓝桥杯真题美丽的2(填空题) C语言/C++

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 小蓝特别喜欢 2&#xff0c;今年是公元 2020 年&#xff0c;他特别高兴。 他很好奇&#xff0c;在公元 1 年到公元 2020 年&#xff08;包含&#xff09;中&#xff…

论文投稿指南——中文核心期刊推荐(电影、电视艺术)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

73. python第三方库安装教程(超详细)

73. python第三方库安装教程&#xff08;超详细&#xff09; 文章目录73. python第三方库安装教程&#xff08;超详细&#xff09;1.知识回顾2. openpyxl 库的作用3. 第三方库的安装步骤【警告内容释义】4. 更新pip5. pip 常用命令1. 什么是pip2. pip --version 查看 pip 版本和…

车载雷达实战之Firmware内存优化

内存&#xff08;Memory&#xff09;是计算机中最重要的部件之一&#xff0c;计算机运时的程序以及数据都依赖它进行存储。内存主要分为随机存储器&#xff08;RAM&#xff09;,只读存储器&#xff08;ROM&#xff09;以及高速缓存&#xff08;Cache&#xff09;。仅仅雷达的原…

树莓派Pico W无线WiFi开发板使用方法及MicroPython编程实践

树莓派Pico W开发板是树莓派基金会于2022年6月底推出的一款无线WiFi开发板&#xff0c;它支持C/C和MicroPython编程。本文介绍树莓派Pico W无线WiFi开发板的使用方法及MicroPython编程示例&#xff0c;包括树莓派Pico W开发板板载LED使用及控制编程示例&#xff0c;Pico W开发板…

Spring——什么是IOC?

一、原则高内聚、低耦合二、什么是IOC&#xff1f;控制反转&#xff0c;把对象创建和对象之间的调用过程&#xff0c;交给spring进行管理三、使用IOC的目的是什么&#xff1f;降低耦合&#xff08;谁和谁的耦合&#xff1f;&#xff1f;如何降低的&#xff1f;&#xff09;原来…

openai-chatGPT的API调用异常处理

因为目前openai对地区限制的原因&#xff0c;即使设置了全局代理使用API调用时&#xff0c;还是会出现科学上网代理的错误问题。openai库 0.26.5【错误提示】&#xff1a;raise error.APIConnectionError(openai.error.APIConnectionError: Error communicating with OpenAI: …

泛型详解.

1 泛型的引入 问题&#xff1a;我们之前实现过的顺序表&#xff0c;只能保存 int 类型的元素&#xff0c;如果现在需要保存 指向 Person 类型对象的引用的顺序表&#xff0c;请问应该如何解决&#xff1f;如果又需要保存指向 Book 对象类型的引用呢&#xff1f; 之前写的顺序表…

红黑树-随记

文章目录1.为什么hashmap用红黑树不用二叉树和平衡二叉树1.1 二叉树&#xff08;Binary Search Tree&#xff09;1.2 红黑树&#xff08;Red Black Tree&#xff09;1.3 平衡二叉树&#xff08;Balence Binary Tree&#xff09;也称AVT2.为什么mysql用b数&#xff0c;不用B数或…

Windows程序员学习Linux环境下VI(VIM)编辑器的使用方法

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天我们来重新审视一下Windows程序员如何学习Linux环境知识。由于很多程序在Windows环境下开发好后&#xff0c;还要部署到Linux服务器上去&#xff0c;所以作为Windows程序员有必要学习Linux环境的知识。VI…

为什么KT6368A双模蓝牙芯片焊到板子上,没反应没收到芯片TX上电返回信息呢

目录 一、问题简介 为什么我把KT6368A芯片焊到板子上面&#xff0c;没有收到芯片TX的脚上电返回信息呢&#xff0c;而KT6368A芯片的2脚一直是2点多v的电压&#xff0c;换了好几个芯片都是这样 二、详细说明 一、问题简介 为什么我把KT6368A芯片焊到板子上面&#xff0c;没有…

QWidgetTable获取选中多行数据

QWidgetTable获取选中的多行数据获取选中行的行编号和打印指定第几列功能快捷键插入链接与图片创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能&#xff0c;丰富你的文章UML 图表FLowchart流…

buu [NPUCTF2020]这是什么觅 1

题目描述&#xff1a; 就一个这种文件&#xff0c;用记事本打开后&#xff1a; 题目分析&#xff1a; 打开后就一串看不懂的东西&#xff0c;想想这个东西曾经在 010editor 或 winhex中出现过&#xff08;右端&#xff09;既然如此那么我们就用它打开&#xff0c;得到&#…

使用 JaCoCo 生成测试覆盖率报告

0、为什么要生成测试覆盖率报告 在我们实际的工作中&#xff0c;当完成程序的开发后&#xff0c;需要提交给测试人员进行测试&#xff0c;经过测试人员测试后&#xff0c;代码才能上线到生产环境。 有个问题是&#xff1a;怎么能证明程序得到了充分的测试&#xff0c;程序中所…

线程池和ThreadLocal详解

线程池和ThreadLocal详解线程池池化模式&#xff1a;线程池里的线程数量设定为多少比较合适?添加线程规则&#xff1a;实现原理&#xff1a;线程池实现任务复用的原理线程池状态&#xff1a;Executors 创线程池工具类手动创建&#xff08;更推荐&#xff09;&#xff1a;自动创…

高码率QPSK调制解调方案(FPGA实现篇)

在前面的章节中,已经讲过QPSK调制的方案和Matlab算法仿真,在本篇中,主要讲解基于FPGA的高速QPSK调制的实现。根据前面提到的技术指标,本系统传输的数据速率为500Mbps,中频为720MHz,因此,传统的串行QPSK调制已经不合适在FPGA中实现,需采用全数字的并行方式进行调制,具体…

电商API是什么?为什么要用?主要应用场景有哪些?

电商API是什么&#xff1f;API是application programming interface&#xff08;应用程序接口&#xff09;的简称&#xff0c;实际上是一些预先定义的函数&#xff0c;目的是提供应用程序与开发人员基于某软件或硬件的以访问一组例程的能力&#xff0c;而又无需访问源码&#x…