ThreadLocal详解

前言

假设在一个项目中,多个线程对一个共享的公共变量/常量进行读取和修改,会出现什么结果呢?
每个线程读取的值是否一致?每个线程修改后的值与期望值是否一致?

下面代码中:

  1. 每个线程期望读取的值为:(0,0)
  2. 每个线程期望修改后输出:(1,10)或者(2,20),在自身使用完后重新赋值:(0,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
    25
    public class Test {
    private volatile int num;
    private static volatile int staticNum;

    public static void main(String[] args) {
    int n = 20;
    Test test = new Test();
    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("1", 1, 10)).start();
    }

    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("2", 2, 20)).start();
    }
    }

    public void print(String prefix, int num, int staticNum) {
    System.out.println(prefix + "-开始: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = num;
    Test.staticNum = staticNum;
    System.out.println(prefix + "-结束: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = 0;
    Test.staticNum = 0;
    }
    }

输出结果截图,不尽人意,或许也在意料之中:

  1. 个别线程读取值为:(1,10)或者(2,20)
  2. 1组中个别线程输出值为:(0,0)或者(2,20)
  3. 2组中个别线程输出值为:(0,0)或者(1,10)

多线程

并发编程3大特性

  1. 原子性:一次或多次操作,要么所有操作全部成功或者全部失败
  2. 可见性:当一个线程修改共享变量时,其他线程可以立即看到修改后的值
  3. 有序性:代码执行顺序必须按照书写顺序执行

Java 4种引用类型

  1. 强引用:使用最多的引用类型,使用 new 方式创建的对象就是强引用。 永远不会被回收,哪怕内存不足
  2. 软引用:使用 SoftReference 修饰的对象。内存溢出时会被回收
  3. 弱引用:使用 WeakReference 修饰的对象。垃圾回收器发生GC就会被回收
  4. 虚引用:使用 PhantomReference 修饰的对象。随时会被回收

线程安全/线程不安全

线程安全/不安全,是指在多线程环境下,对于同一份数据的访问是否能够保证其正确性和一致性的描述。

  1. 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
  2. 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

上面的情况,称为线程不安全。一般提供3种解决方案:

  1. volatile,保证可见性、防重排序。但是对于上面的场景明显没效果
  2. synchronized,线程安全
    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
    public class Test {
    private volatile int num;
    private static volatile int staticNum;

    public static void main(String[] args) {
    int n = 20;
    Test test = new Test();
    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("1", 1, 10)).start();
    }

    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("2", 2, 20)).start();
    }
    }

    // 方法加synchronized
    public synchronized void print(String prefix, int num, int staticNum) {
    System.out.println(prefix + "-开始: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = num;
    Test.staticNum = staticNum;
    System.out.println(prefix + "-结束: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = 0;
    Test.staticNum = 0;
    }
    }
  3. ThreadLocal,线程副本
    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
    public class Test {
    private volatile int num;
    private static volatile int staticNum;

    private final static ThreadLocal<Test> LOCAL = ThreadLocal.withInitial(Test::new);

    public static void main(String[] args) {
    int n = 20;
    Test test = new Test();
    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("1", 1, 10)).start();
    }

    for (int i = 0; i < n; i++) {
    new Thread(() -> test.print("2", 2, 20)).start();
    }
    }

    public void print(String prefix, int num, int staticNum) {
    System.out.println(prefix + "-开始: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = num;
    Test.staticNum = staticNum;
    LOCAL.set(this);
    System.out.println(prefix + "-结束: " + Thread.currentThread().getName() + ", num=" + this.num + ", staticNum=" + Test.staticNum);
    this.num = 0;
    Test.staticNum = 0;
    LOCAL.remove();
    }
    }

内存泄漏

内存泄漏是指应用程序中分配的内存(通常是堆内存)在不再需要时未能正确释放。
也就是说,本该被回收的对象未能被回收,而这些未被回收释放的对象(内存)会累积,
最终造成内存资源被消耗完全,应用程序异常错误甚至崩溃的情况。

Thread 内部结构

再看看 Thread 内部结构,如图:

Thread 内部有一个 ThreadLocalMap 类型的变量 threadLocals
inheritableThreadLocals 两个变量,也就是说,每个线程都有自己的 ThreadLocalMap

  1. threadLocals:保存 thread 自身的数据副本数据
  2. inheritableThreadLocals:保存 thread 父线程的数据副本

ThreadLocalMap 类似 HashMap结构,但是没有链表,内部只维护 Entry 数组,
同时Entry 继承 WeakReference(弱引用类型),
keyThreadLocal 本身,valueThreadLocal 指定泛型对象。

ThreadLocal 是线程安全的

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

ThreadLocal 内存泄漏问题

ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key ,当 ThreadLocal 变量
被手动赋值为 null(即ThreadLocal对象没有外部强引用来引用它),当发生GC时,ThreadLocal
一定会被回收。ThreadLocalMap 中会存在 key 为null的 EntryEntry 中的 value
由于 key 为null无法被访问。如果当前线程迟迟没有结束(线程池中的线程),value 永远存在,从而
造成内存泄漏。所以在使用完,手动调用 remove 方法进行释放

ThreadLocal 变量正常使用不为null:

ThreadLocal 变量手动赋值 null:

ThreadLocal 源码解析

set 方法

get方法

remove方法