Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

    • 多线程
    • CompletableFuture
    • 锁
    • LockSupport与线程中断
    • Java内存模型之JMM
    • Volatile与Java内存模型
    • CAS
    • 原子操作类
    • ThreadLocal
      • ThreadLocal简介
        • 案例
        • 小结
      • 阿里ThreadLocal规范
        • 非线程安全的SimpleDateFormat
        • 源码分析结论
        • 解决方案1
        • 解决方案2
        • 其他方案
      • ThreadLocal源码分析
      • ThreadLocal内存泄露问题
        • 强引用、软引用、弱引用、虚引用分别是什么?
        • 再回首ThreadLocalMap
        • 整体架构
        • 强引用(默认支持模式)
        • 软引用
        • 弱引用
        • 软引用和弱引用的适用场景
        • 虚引用
        • GCRoots和四大引用小总结
        • 关系
        • 为什么要用弱引用?不用如何?
        • 弱引用就万事大吉了吗?
        • key为null的entry,原理解析
        • set、get方法会去检查所有键为null的Entry对象
        • 总结
    • Java对象内存布局和对象头
    • Synchronized与锁升级
    • AbstractQueuedSynchronizer之AQS
    • ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
    • JUC总结
    • 并发集合
  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • JUC
Iekr
2023-12-04
目录

ThreadLocal

# ThreadLocal

# ThreadLocal 简介

image-20231204085234013

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

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

image-20231204085342112

api 方法

image-20231204085352152

# 案例

按照总销售额统计,方便集团公司做计划统计

package com.atguigu.juc.tl;


import java.util.concurrent.TimeUnit;

class MovieTicket
{
    int number = 50;

    public synchronized void saleTicket()
    {
        if(number > 0)
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
        }else{
            System.out.println("--------卖完了");
        }
    }
}

/**
 * @auther zzyy
 * @create 2021-03-23 15:03
 * 三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
 */
public class ThreadLocalDemo
{
    public static void main(String[] args)
    {
        MovieTicket movieTicket = new MovieTicket();

        for (int i = 1; i <=3; i++) {
            new Thread(() -> {
                for (int j = 0; j <20; j++) {
                    movieTicket.saleTicket();
                    try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            },String.valueOf(i)).start();
        }
    }
}
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
41

上述需求变化了... 不参加总和计算,希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计。比如某找房软件,每个中介销售都有自己的销售额指标,自己专属自己的,不和别人掺和。

package com.atguigu.juc.tl;


class MovieTicket
{
    int number = 50;

    public synchronized void saleTicket()
    {
        if(number > 0)
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
        }else{
            System.out.println("--------卖完了");
        }
    }
}

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

    public void saleHouse()
    {
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

/**
 * @auther zzyy
 * @create 2021-03-23 15:03
 * 1  三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
 *
 * 2  分灶吃饭,各个销售自己动手,丰衣足食
 */
public class ThreadLocalDemo
{
    public static void main(String[] args)
    {
        /*MovieTicket movieTicket = new MovieTicket();

        for (int i = 1; i <=3; i++) {
            new Thread(() -> {
                for (int j = 0; j <20; j++) {
                    movieTicket.saleTicket();
                    try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            },String.valueOf(i)).start();
        }*/

        //===========================================
        House house = new House();

        new Thread(() -> {
            try {
                for (int i = 1; i <=3; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            }finally {
                house.threadLocal.remove();//如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
            }
        },"t1").start();

        new Thread(() -> {
            try {
                for (int i = 1; i <=2; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            }finally {
                house.threadLocal.remove();
            }
        },"t2").start();

        new Thread(() -> {
            try {
                for (int i = 1; i <=5; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            }finally {
                house.threadLocal.remove();
            }
        },"t3").start();


        System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
    }
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 小结

  • 因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
  • 既然其它 Thread 不可访问,那就不存在多线程间共享的问题。
  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
  • 一句话:如何才能不争抢
    • 加入 synchronized 或者 Lock 控制资源的访问顺序
    • 人手一份,大家各自安好,没必要抢夺

# 阿里 ThreadLocal 规范

# 非线程安全的 SimpleDateFormat

image-20231204085614309

SimpleDateFormat 中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

写时间工具类,一般写成静态的成员变量,不知,此种写法的多线程下的危险性!

package com.atguigu.itdachang;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @auther zzyy
 * @create 2020-07-17 16:42
 */
public class DateUtils
{
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
     * @param stringDate
     * @return
     * @throws Exception
     */
    public static Date parseDate(String stringDate)throws Exception
    {
        return sdf.parse(stringDate);
    }
    
    public static void main(String[] args) throws Exception
    {
        for (int i = 1; i <=30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
 
 

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

image-20231204085653407

# 源码分析结论

SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如 sdf.parse (dateStr),sdf.format (date) 诸如此类的方法参数传入的日期相关 String,Date 等等,都是交由 Calendar 引用来储存的。这样就会导致一个问题如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat, 同时也是共享这个 Calendar 引用。

image-20231204085742751

image-20231204085747748

# 解决方案 1

将 SimpleDateFormat 定义成局部变量。

缺点:每调用一次方法就会创建一个 SimpleDateFormat 对象,方法结束又要作为垃圾回收。

package com.atguigu.itdachang;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @auther zzyy
 * @create 2020-07-17 16:42
 */
public class DateUtils
{
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
     * @param stringDate
     * @return
     * @throws Exception
     */
    public static Date parseDate(String stringDate)throws Exception
    {
        return sdf.parse(stringDate);
    }

    public static void main(String[] args) throws Exception
    {
        for (int i = 1; i <=30; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(sdf.parse("2020-11-11 11:11:11"));
                    sdf = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
 

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

# 解决方案 2

ThreadLocal,也叫做线程本地变量或者线程本地存储

package com.atguigu.itdachang;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @auther zzyy
 * @create 2020-07-17 16:42
 */
public class DateUtils
{
    private static final ThreadLocal<SimpleDateFormat>  sdf_threadLocal =
            ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    /**
     * ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
     * @param stringDate
     * @return
     * @throws Exception
     */
    public static Date parseDateTL(String stringDate)throws Exception
    {
        return sdf_threadLocal.get().parse(stringDate);
    }

    public static void main(String[] args) throws Exception
    {
        for (int i = 1; i <=30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
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

# 其他方案

  • 加锁
  • 第三方时间库

DateUtils

package com.atguigu.juc.senior.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @auther zzyy
 * @create 2020-05-03 10:14
 */
public class DateUtils
{
    /*
    1   SimpleDateFormat如果多线程共用是线程不安全的类
    public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date)
    {
        return SIMPLE_DATE_FORMAT.format(date);
    }

    public static Date parse(String datetime) throws ParseException
    {
        return SIMPLE_DATE_FORMAT.parse(datetime);
    }*/

    //2   ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
    public static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date)
    {
        return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().format(date);
    }

    public static Date parse(String datetime) throws ParseException
    {
        return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(datetime);
    }


    //3 DateTimeFormatter 代替 SimpleDateFormat
    /*public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String format(LocalDateTime localDateTime)
    {
        return DATE_TIME_FORMAT.format(localDateTime);
    }

    public static LocalDateTime parse(String dateString)
    {

        return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
    }*/
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# ThreadLocal 源码分析

Thread,ThreadLocal,ThreadLocalMap 关系

Thread 和 ThreadLocal

image-20231204090007605

ThreadLocal 和 ThreadLocalMap

image-20231204090032707

三者总概括

image-20231204090044567

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

当我们为 threadLocal 变量赋值,实际上就是以当前 threadLocal 实例为 key,值为 value 的 Entry 往这个 threadLocalMap 中存。

近似的可以理解为: ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的 map (其实是以 ThreadLocal 为 Key),不过是经过了两层包装的 ThreadLocal 对象:

image-20231204090147764

JVM 内部维护了一个线程版的 Map<Thread,T>(通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中), 每个线程要用到这个 T 的时候,用当前的线程去 Map 里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

# ThreadLocal 内存泄露问题

image-20231204090405131

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

# 强引用、软引用、弱引用、虚引用分别是什么?

# 再回首 ThreadLocalMap

image-20231204090434241

image-20231204090147764

**ThreadLocalMap 与 WeakReference **

ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的 map (其实是以它为 Key),不过是经过了两层包装的 ThreadLocal 对象:

  1. 第一层包装是使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用的对象;
  2. 第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>:

# 整体架构

image-20231204090643382

Java 技术允许使用 finalize () 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

image-20231204090704014

新建一个带 finalize () 方法的对象 MyObject

 
class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了讲课演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}
1
2
3
4
5
6
7
8
9
10

# 强引用 (默认支持模式)

当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收,死都不收。

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了 (当然具体回收时机还是要看垃圾收集策略)。

 
public static void strongReference()
{
    MyObject myObject = new MyObject();
    System.out.println("-----gc before: "+myObject);

    myObject = null;
    System.gc();
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

    System.out.println("-----gc after: "+myObject);
}
1
2
3
4
5
6
7
8
9
10
11
12

# 软引用

软引用是一种相对强引用弱化了一些的引用,需要用 java.lang.ref.SoftReference 类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说:

  • 当系统内存充足时它 不会 被回收,
  • 当系统内存不足时它 会 被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

package com.atguigu.juc.tl;

import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;

class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了讲课演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}

/**
 * @auther zzyy
 * @create 2021-03-24 10:31
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());

        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());

        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }

    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);

        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("-----gc after: "+myObject);
    }
}
 
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
41
42
43
44
45
46
47
48
49
50
51
52
53

# 弱引用

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

package com.atguigu.juc.tl;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了讲课演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}

/**
 * @auther zzyy
 * @create 2021-03-24 10:31
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-----gc before内存够用: "+weakReference.get());

        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("-----gc after内存够用: "+weakReference.get());
    }

    public static void softReference()
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());

        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());

        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }

    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);

        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("-----gc after: "+myObject);
    }
}
 
 

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 软引用和弱引用的适用场景

假如有一个应用需要读取大量的本地图片:

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

此时使用软引用可以解决这个问题。

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

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

# 虚引用

虚引用需要 java.lang.ref.PhantomReference 类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收, 它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue) 联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。 PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。

其意义在于:说明一个对象已经进入 finalization 阶段,可以被 gc 回收,用来实现比 finalization 机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

构造方法

image-20231204091144555

引用队列

image-20231204091154572

被回收前需要被引用队列保存下。

package com.atguigu.juc.tl;

import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了讲课演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}

/**
 * @auther zzyy
 * @create 2021-03-24 10:31
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
        //System.out.println(phantomReference.get());

        List<byte[]> list = new ArrayList<>();

        new Thread(() -> {
            while (true)
            {
                list.add(new byte[1 * 1024 * 1024]);
                try { TimeUnit.MILLISECONDS.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(phantomReference.get());
            }
        },"t1").start();

        new Thread(() -> {
            while (true)
            {
                Reference<? extends MyObject> reference = referenceQueue.poll();
                if (reference != null) {
                    System.out.println("***********有虚对象加入队列了");
                }
            }
        },"t2").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    public static void weakReference()
    {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-----gc before内存够用: "+weakReference.get());

        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("-----gc after内存够用: "+weakReference.get());
    }

    public static void softReference()
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());

        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());

        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }

    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);

        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("-----gc after: "+myObject);
    }
}
 
 

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

# GCRoots 和四大引用小总结

image-20231204091214709

# 关系

image-20231204091257313

image-20231204090147764

每个 Thread 对象维护着一个 ThreadLocalMap 的引用

ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储

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

# 为什么要用弱引用?不用如何?

public void function01()
{
    ThreadLocal tl = new ThreadLocal<Integer>();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}

1
2
3
4
5
6
7

line1 新建了一个 ThreadLocal 对象,t1 是强引用指向这个对象; line2 调用 set () 方法后新建一个 Entry,通过源码可知 Entry 对象里的 k 是弱引用指向这个对象。

image-20231204091409023

为什么源代码用弱引用? 当 function01 方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象

  • 若这个 key 引用是强引用,就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收,造成内存泄漏;
  • 若这个 key 引用是弱引用就大概率会减少内存泄漏的问题 (还有一个 key 为 null 的雷)。使用弱引用,就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null。

此后我们调用 get,set 或 remove 方法时,就会尝试删除 key 为 null 的 entry,可以释放 value 对象所占用的内存。

# 弱引用就万事大吉了吗?

此后我们调用 get,set 或 remove 方法时,就会尝试删除 key 为 null 的 entry,可以释放 value 对象所占用的内存

image-20231204091409023

  1. 当我们为 threadLocal 变量赋值,实际上就是当前的 Entry (threadLocal 实例为 key,值为 value) 往这个 threadLocalMap 中存放。Entry 中的 key 是弱引用,当 threadLocal 外部强引用被置为 null (tl=null), 那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
  2. 当然,如果当前 thread 运行结束,threadLocal,threadLocalMap,Entry 没有引用链可达,在垃圾回收的时候都会被系统进行回收。
  3. 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在 Executors.newFixedThreadPool () 时创建线程的时候,为了复用线程是不会结束的,所以 threadLocal 内存泄漏就值得我们小心

# key 为 null 的 entry,原理解析

image-20231204091257313

ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用引用他,那么系统 gc 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话 (比如正好用在线程池),这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链。

虽然弱引用,保证了 key 指向的 ThreadLocal 对象能被及时回收,但是 v 指向的 value 对象是需要 ThreadLocalMap 调用 get、set 时发现 key 为 null 时才会去回收整个 entry、value,因此弱引用不能 100% 保证内存不泄露。我们要在不使用某个 ThreadLocal 对象后,手动调用 remoev 方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果我们不手动调用 remove 方法,那么后面的线程就有可能获取到上个线程遗留下来的 value 值,造成 bug。

# set、get 方法会去检查所有键为 null 的 Entry 对象

set、get 方法会去检查所有键为 null 的 Entry 对象

set()

image-20231204091734725

image-20231204091739252

get()

image-20231204091758446

image-20231204091814958

image-20231204091821091

remove()

image-20231204091832850

从前面的 set,getEntry,remove 方法看出,在 threadLocal 的生命周期里,针对 threadLocal 存在的内存泄漏的问题,都会通过 expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法清理掉 key 为 null 的脏 entry。

# 总结

image-20231204091936656

image-20231204092032402

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • 都会通过 expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
  • 群雄逐鹿起纷争,人各一份天下安
编辑 (opens new window)
上次更新: 2023/12/13, 06:06:02
原子操作类
Java对象内存布局和对象头

← 原子操作类 Java对象内存布局和对象头→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式