Marvel-Site Marvel-Site
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)

Marvel

吾必当乘此羽葆盖车
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)
  • Java

    • Java基础

    • Java进阶

    • Java容器

    • Java并发编程

      • 并发编程基本概念
      • 多线程
      • 创建多线程的方法
      • 比较与交换CAS
      • Java对象头与Monitor监视器
      • Java主流锁
      • synchronized关键字
      • volatile关键字
      • 线程池
      • 原子类Atomic
      • LockSupport工具
      • 抽象队列同步器AQS
      • ThreadLocal深度理解
        • ThreadLocal简介
        • ThreadLocal数据结构
        • ThreadLocal中的弱引用
        • ThreadLocal内存泄露问题
        • InheritableThreadLocal
      • 多线程循环打印代码
      • 线程等待和唤醒的三种方式
      • ReentrantLock非公平锁的源码分析
    • Java虚拟机

    • 常见面试题

  • 计算机基础

  • 框架|中间件

  • 架构

  • 后端
  • Java
  • Java并发编程
Marvel
2022-07-13
目录

ThreadLocal深度理解

# ThreadLocal深度理解

# ThreadLocal简介

通常情况下,我们创建的变量可以被任何一个线程访问并修改的。如果想实现每一个线程都自己的专属本地变量可以使用 ThreadLocal类,ThreadLocal 类主要解决的是让每个线程绑定自己的值。

如果创建了一个 ThreadLocal 变量,那么访问这个变量的线程都会有这个变量的本地副本。他们可以使用get()和set()方法来获取默认值或将其值更改为当前线程所存放的副本的值,避免线程安全问题。

⭐ ThreadLocal示例

创建一个存储 String 类型的 ThreadLocal 对象,并在 A 个线程中分别为对象赋值。依次在 A 线程和 B 线程和主线程中获取 ThreadLocal 对象的值。

public class ThreadLocalTest {
    public static ThreadLocal<String> holder = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        /* 创建A线程并启动 */
        new Thread(() -> {
            holder.set("A");  // B线程设置holder值
            System.out.println("线程" + Thread.currentThread().getName() + ": " + holder.get());  // A线程获取holder值
        }, "A").start();

        /* 创建B线程并启动 */
        new Thread(() -> {
            try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("线程" + Thread.currentThread().getName() + ": " + holder.get());  // B线程获取holder值
        }, "B").start();

        Thread.sleep(300);

        System.out.println(Thread.currentThread().getName() + ": " + holder.get()); // main线程获取holder值
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

⭐ 控制台输出

线程A: A
线程B: null
main: null
1
2
3

对于 A 线程,holder的值为 A;对于 B 线程和 main 线程,holder 的值为空,因为我们从未对其赋值。

# ThreadLocal数据结构

Thread类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

image-20220817143847766

# ThreadLocal中的弱引用

在看这部分之前,一定要先了解 Java 的强、软、弱、虚四大引用。

ThreadLocal 的弱引用体现在 ThreadLocalMap 中的 Entry 上。

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
1
2
3
4
5
6
7
8
9

对于这个 Entry,它的键是弱引用,引用一个 ThreadLocal 对象;值是强引用,可以引用任意对象。

image-20220817143729896

对于每一个线程都有属于自己的 ThreadLocalMap,从下面这段源码可以看出



 



public class Thread implements Runnable {
	...
	ThreadLocal.ThreadLocalMap threadLocals = null;
	...
}
1
2
3
4
5

对于简介中的示例代码,他们的引用关系应该是这样:

  • ThreadLocal对象引用 与 ThreadLocal对象 之间引用是「强引用」
  • ThreadLocalMap.Entry中的key 与 ThreadLocal对象之间的引用是「弱引用」
  • ThreadLocalMap.Entry中的value 与 引用的对象之间的引用是「强引用」
image-20220817143749154

从 JVM 内存结构上看它们的更为具体的引用关系,首先我们要知道:

  • 堆是线程共享的,栈是线程私有的
  • 实例对象存储在堆中,静态变量的引用存储在方法区中,静态变量的实例对象存储在堆中。

简介示例代码中的 ThreadLocal 是类的静态变量,所以他的对象引用存储在方法区,而不是主线程的栈中。

public class ThreadLocalTest {
    public static ThreadLocal<String> holder = new ThreadLocal<>();
 	...   
}
1
2
3
4

如果我们在 main 方法中实例化 ThreadLocal 对象,那么其他线程是无法获取到这个对象的,因为这个对象只存储在 main 线程的栈中。所以只有当 ThreadLocal 为静态变量时,其他的线程才能访问到。

image-20220817143809928

当我们令 holder = null,并且进行了垃圾回收后,ThreadLocal 对象将会被垃圾回收器回收。因为 Entry 的 key 是弱引用,所以不会影响垃圾回收器对 ThreadLocal 对象的回收,此时 key 的值为 null,但 value 的值不为 null,这将导致内存泄露问题。

image-20220817143824587

# ThreadLocal内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value永远无法被GC回收,这时候就产生了内存泄漏。

ThreadLocalMap实现已经考虑了这种情况,在调用 set、get、remove 方法的时候,会清理掉 key 为 null 的记录。所以在使用ThreadLocal 方法后,最好手动调用 remove 方法。

# InheritableThreadLocal

启动另外一个线程,那么在主线程设置的 threadlocal 值如何能被子线程拿到?

正常情况

public class ThreadLocalTest1 {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("test");
        System.out.println("outer thread is: " + Thread.currentThread().getName() + ", value is: " + threadLocal.get());

        new Thread(() -> {
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("inner thread is: " + Thread.currentThread().getName() + ", value is: " + threadLocal.get());
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

输出为:

outer thread is: main, value is: test inner thread is: Thread-0, value is: null

可以使用 InheritableThreadLocal,可继承的 ThreadLocal

public class ThreadLocalTest1 {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set("test");
        System.out.println("outer thread is: " + Thread.currentThread().getName() + ", value is: " + threadLocal.get());

        new Thread(() -> {
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("inner thread is: " + Thread.currentThread().getName() + ", value is: " + threadLocal.get());
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

输出为:

outer thread is: main, value is: test inner thread is: Thread-0, value is: test

InheritableThreadLocal 的原理:

Thread 类里面有 threadLocals 和 inheritableThreadLocals 两个类变量

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
1
2

在 Thread 构造函数中,有这样一段代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
1
2
3

也就是会去获得父 inheritableThreadLocals 的值,并赋值给自己。

编辑 (opens new window)
#Java#JUC
上次更新: 2024/04/29, 10:25:44
抽象队列同步器AQS
多线程循环打印代码

← 抽象队列同步器AQS 多线程循环打印代码→

最近更新
01
位运算
05-21
02
二叉树
05-12
03
Spring三级缓存解决循环依赖
03-25
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Marvel
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式