• Java多线程学习入门(五):ThreadLocal以及对象内存布局


    开始时间:2022-09-13
    课程链接:尚硅谷2022版JUC并发编程
    JavaGuide

    ThreadLocal

    看一个Demo
    没有ThreadLocal

    package com.bupt.threadLocalDemo;
    
    import java.util.Random;
    
    public class ThreadLocalDemo1 {
        public static void main(String[] args) {
            House house = new House();
            for (int i = 1; i <= 5; i++) {
                new Thread(() -> {
                    int size = new Random().nextInt(5) + 1;
                    System.out.println(size);
                    for (int j = 1; j <= size; j++) {
                        house.saleHouse();
                    }
                }, "A").start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("总共卖了" + house.saleCount + "套房子");
        }
    }
    
    class House {
        int saleCount = 0;
    
        public synchronized void saleHouse() {
            ++saleCount;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    输出

    4
    4
    2
    5
    3
    总共卖了18套房子
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那我想获得每个销售人员的销售额怎么办呢?
    用ThreadLocal

    package com.bupt.threadLocalDemo;
    
    import java.util.Random;
    
    public class ThreadLocalDemo1 {
        public static void main(String[] args) {
            House house = new House();
            for (int i = 1; i <= 5; i++) {
                new Thread(() -> {
                    int size = new Random().nextInt(5) + 1;
                    //System.out.println(size);
                    for (int j = 1; j <= size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + " " + "号销售卖出:" + house.saleVolume.get());
                }, String.valueOf(i)).start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("总共卖了" + house.saleCount + "套房子");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    class House {
        int saleCount = 0;
        //初始化
        ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
    
        public void saleVolumeByThreadLocal() {
            saleVolume.set(1 + saleVolume.get());
        }
    
        public synchronized void saleHouse() {
            ++saleCount;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    3 号销售卖出:1
    2 号销售卖出:4
    1 号销售卖出:4
    4 号销售卖出:3
    5 号销售卖出:2
    总共卖了14套房子
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ThreadLocal回收

    【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑造成内存泄露等问题尽量在代理中使用
    try-finally块进行回收。

    还是刚才的例子

    for (int i = 1; i <= 5; i++) {
                new Thread(() -> {
                    int size = new Random().nextInt(5) + 1;
                    try {
                        for (int j = 1; j <= size; j++) {
                            house.saleHouse();
                            house.saleVolumeByThreadLocal();
                        }
                        System.out.println(Thread.currentThread().getName() + " " + "号销售卖出:" + house.saleVolume.get());
                    } finally {
                        house.saleVolume.remove();
                    }
                }, String.valueOf(i)).start();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们来看看如果不回收会是什么情况
    我们比如说通过线程池里面的线程来实现复用
    比如我们一共用到了的也就是核心线程数5
    如果不回收,当我执行超过5的线程时,他会发生线程复用,这个很好理解,
    但是此时我们用到的第六个线程,我们希望他开的是一个全新的线程,即本地变量应该是初始值的样子,但实际他会保留之前上一个用完这个线程时的值
    看一个例子

    public class ThreadLocalDemo2 {
        private static final int CORE_POOL_SIZE = 5;
        private static final int MAX_POOL_SIZE = 15;
        private static final int QUEUE_CAPACITY = 80;
        private static final Long KEEP_ALIVE_TIME = 1L;
    
        public static void main(String[] args) {
            MyData myData = new MyData();
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                    new ThreadPoolExecutor.CallerRunsPolicy());
            try {
                for (int i = 0; i < 20; i++) {
                    threadPoolExecutor.submit(() -> {
                        try {
                            Integer beforeInt = myData.threadLocalField.get();
                            myData.add();
                            Integer afterInt = myData.threadLocalField.get();
                            System.out.println(Thread.currentThread().getName() + " beforeInt: " + beforeInt + " afterInt: " + afterInt);
                        } finally {
                            //myData.threadLocalField.remove();
                        }
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPoolExecutor.shutdown();
            }
            //终止线程池
    
            while (!threadPoolExecutor.isTerminated()) {
            }
            System.out.println("Finished all threads");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    class MyData {
        ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    
        public void add() {
            threadLocalField.set(threadLocalField.get() + 1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们没有进行回收,看看结果

    pool-1-thread-2 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-3 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-1 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 1 afterInt: 2
    pool-1-thread-3 beforeInt: 1 afterInt: 2
    pool-1-thread-5 beforeInt: 1 afterInt: 2
    pool-1-thread-3 beforeInt: 2 afterInt: 3
    pool-1-thread-2 beforeInt: 1 afterInt: 2
    pool-1-thread-3 beforeInt: 3 afterInt: 4
    pool-1-thread-5 beforeInt: 2 afterInt: 3
    pool-1-thread-4 beforeInt: 2 afterInt: 3
    pool-1-thread-1 beforeInt: 1 afterInt: 2
    pool-1-thread-4 beforeInt: 3 afterInt: 4
    pool-1-thread-5 beforeInt: 3 afterInt: 4
    pool-1-thread-3 beforeInt: 4 afterInt: 5
    pool-1-thread-2 beforeInt: 2 afterInt: 3
    pool-1-thread-4 beforeInt: 4 afterInt: 5
    pool-1-thread-1 beforeInt: 2 afterInt: 3
    Finished all threads
    
    Process finished with exit code 0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可以看到,在第六个使用时,他就保留了上一次的本地变量结果,这不是我们希望的(除非特定要求),那我们应该用finally给他回收掉

    try {
                            Integer beforeInt = myData.threadLocalField.get();
                            myData.add();
                            Integer afterInt = myData.threadLocalField.get();
                            System.out.println(Thread.currentThread().getName() + " beforeInt: " + beforeInt + " afterInt: " + afterInt);
                        } finally {
                            myData.threadLocalField.remove();
                        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    再看看结果

    pool-1-thread-1 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-3 beforeInt: 0 afterInt: 1
    pool-1-thread-2 beforeInt: 0 afterInt: 1
    pool-1-thread-3 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-1 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-3 beforeInt: 0 afterInt: 1
    pool-1-thread-2 beforeInt: 0 afterInt: 1
    pool-1-thread-3 beforeInt: 0 afterInt: 1
    pool-1-thread-5 beforeInt: 0 afterInt: 1
    pool-1-thread-4 beforeInt: 0 afterInt: 1
    pool-1-thread-1 beforeInt: 0 afterInt: 1
    pool-1-thread-2 beforeInt: 0 afterInt: 1
    Finished all threads
    
    Process finished with exit code 0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    Thread、ThreadLocal 、ThreadLocalMap

    每个Thread里面都有一个ThreadLocal
    ThreadLocal里面有静态内部类ThreadLocalMap
    而这个静态内部类ThreadLocalMap里面有一个静态内部类Entry
    这个Entry继承了WeakReference>
    这个Entry的Key就是ThreadLocal,Value就是一个值

    threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。

    老师的举例
    在这里插入图片描述
    Thread 自然人
    身份证本身(卡片) ThreadLocal
    信息内容 身份证:名字
    ThreadLocalMap

    ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

    1)调用ThreadLocal的set)方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象

    2)调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
    ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocalMap获取value。正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响于

    ThreadLocal弱引用问题

    刚刚上面使用的例子解释了不回收ThreadLocal导致业务逻辑出问题的情况
    但是没有讲到不回收造成线程泄露的情况,这里来补充

    内存泄露:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

    强引用如果一个对象具有强引用,那垃圾回收器不会回收它
    软引用内存空间不足了,才会回收这些对象的内存
    弱引用一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存,挺不过下一次GC
    虚引用如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

    我们来看看应用场景
    假如有一个应用需要读取大量的本地图片:

    • 如果每次读取图片都从硬盘读取则会严重影响性能,
    • 如果一次性全部加载到内存中又可能造成内存溢出。

    此时使用软引用可以解决这个问题。
    设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

    Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String,
    SoftReference<Bitmap>>();
    
    • 1
    • 2

    虚引用

    换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作

    虚引用和引用队列是结合着用的
    每次虚引用发生后,就会放到引用队列里面

    为什么ThreadLocal源代码中key用弱引用?
    当fumnctionl1方法执行完毕后,栈帧销毁强引用tl也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
    若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
    若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷,第2个坑后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。(这里的tl就是外部强引用)
    在这里插入图片描述
    但是ThreadLocalMap出现key为null的Entry,就没有办法访问这些key为null的Entry的value了
    如果当前线程迟迟不结束,这些key为null的Entry的value就会一直存在一条强引用链,value永远无法回收,造成内存泄露
    我们用线程池的方式,就容易迟迟不结束
    弱引用保证了key指向的ThreadLocal对象能及时回收,但是v指向的value还在
    所以我们要手动remove

    GC Roots

    哪些对象可以作为 GC Roots 呢?

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 本地方法栈(Native 方法)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 所有被同步锁持有的对象

    对象内存布局

    对象头(对象标记、类元信息)-实例数据-对齐填充(保证8个字节的整数)
    在这里插入图片描述

    对象头

    对象头分为对象标记(Mark Word)和类元信息

    对象标记

    对象标记里面又装了哈希码(object.hashcode()),GC标记,GC次数(承受GC大镰刀的次数,或者叫GC年龄,一般4个bit,最大存15),同步锁标记(synchronize有没有锁这个对象,从这里标记的),偏向锁持有者
    在这里插入图片描述
    MarkWord占了8个字节

    类元信息(类型指针)

    Class Pointer 占8个字节(暂时不考虑压缩指针的情况)

    主要用来指向方法区的类元信息,也就是我这个对象实例对应到哪个类的

    实例数据

    存放类的属性(Field)数据信息
    包括父类的属性信息

    对齐填充

    补齐8字节的整数倍

    比如我的类里面有一个int,一个boolean
    那么总共字节就是8+8+4+1=21字节
    需要补3个字节对齐

    在这里插入图片描述

    对象分代年龄

    默认是4个bit,也就对应十进制0-15
    如果我们设置为16就要报错
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    结束时间:20220915

  • 相关阅读:
    由浅入深,带你搞懂 Pytorch 中的张量 tensor 是什么
    安装TensorRT
    探秘Nutch:揭秘开源搜索引擎的工作原理与无限应用可能(三)
    gorm day8
    人工智能|机器学习——K-means系列聚类算法k-means/ k-modes/ k-prototypes/ ......(划分聚类)
    浪潮信息面向全行业公布设计指南,以开放规范促进生成式AI多元算力发展
    FineReport可视化数据图表- 函数计算组成和语法示例
    VUE项目中页面权限和按钮权限
    如何区分小角X射线散射和小角X射线衍射?
    为什么我不推荐去SAP培训机构参加培训?
  • 原文地址:https://blog.csdn.net/qq_43568982/article/details/126829542