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

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对象本身,防止内存泄漏,属于安全加固方法
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值