第九章 聊聊 ThreadLocal

9.1 ThreadLocal 简介

9.1.1 面试题

  • ThreadLocal 中 ThreadLocalMap 的数据结构和关系?

  • ThreadLocal 中 key 是弱引用,这是为什么?

  • ThreadLocal 内存泄漏问题你知道嘛?

  • ThreadLocal 中最后为什么要加 remove 方法?

9.1.2 是什么?

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

9.1.3 能干吗?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量,不用麻烦别人,不和其他人共享,人人有份,人各一份)。

主要解决了让每个线程绑定自己的值,通过使用get()set()方法,获取默认值或将其改为当前线程所存的副本的值,从而避免了线程安全问题。比如 8 锁案例中,资源类是使用同一部手机,多个线程抢夺同一部手机,假如人手一份不是天下太平?

9.1.4 API 介绍

image

9.1.5 永远的 helloworld 讲起

问题描述:5个销售买房子,集团只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金----群雄逐鹿起纷争----为了数据安全只能加锁

public class ThreadLocalDemo {

    /**
     * 需求:5个销售卖房子,集团只关心销售总量的精确统计数
     *
     * 运行结果:
     * 2
     * 5
     * 1
     * 5
     * 5
     * main	共计卖出:18
     */
    public static void main(String[] args) {
        House house = new House();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                System.out.println(size);
                for (int j = 0; j < size; j++) {
                    house.saleHouse();
                }
            }, String.valueOf(i)).start();
        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出:" + house.saleCount);
    }

}

class House {

    int saleCount = 0;
    public synchronized void saleHouse() {
        ++saleCount;
    }

}

需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计----比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。----人手一份天下安

public class ThreadLocalDemo {

    /**
     * 需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计-------
     * 比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。
     *
     * 运行结果:
     * 2	号销售卖出:2
     * 3	号销售卖出:4
     * 0	号销售卖出:4
     * 4	号销售卖出:4
     * 1	号销售卖出:2
     * main	共计卖出:16
     */
    public static void main(String[] args) {
        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.saleHouse();
                    house.saleVolumeByThreadLocal();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
            }, String.valueOf(i)).start();
        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出:" + house.saleCount);

    }

}

class House {

    int saleCount = 0;
    public synchronized void saleHouse() {
        ++saleCount;
    }

    /*ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };*/
    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

    public void saleVolumeByThreadLocal() {
        saleVolume.set(threadLocal.get() + 1);
    }

}

增加清理逻辑

public class ThreadLocalDemo {

    /**
     * 需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计-------
     * 比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。
     *
     * 运行结果:
     * 2	号销售卖出:2
     * 3	号销售卖出:4
     * 0	号销售卖出:4
     * 4	号销售卖出:4
     * 1	号销售卖出:2
     * main	共计卖出:16
     */
    public static void main(String[] args) {
        House house = new House();
        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.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
                } finally {
                    house.saleVolume.remove();
                }
            }, String.valueOf(i)).start();
        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出:" + house.saleCount);

    }

}

class House {

    int saleCount = 0;
    public synchronized void saleHouse() {
        ++saleCount;
    }

    /*ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };*/
    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

    public void saleVolumeByThreadLocal() {
        saleVolume.set(saleVolume.get() + 1);
    }

}

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

线程池演示

public class ThreadLocalDemo2 {

    /**
     * 不清理
     *
     * 运行结果:
     * 
     * pool-1-thread-1	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-3	beforeInt:0	afterInt:1
     * pool-1-thread-1	beforeInt:1	afterInt:2
     * pool-1-thread-1	beforeInt:2	afterInt:3
     * pool-1-thread-1	beforeInt:3	afterInt:4
     * pool-1-thread-1	beforeInt:4	afterInt:5
     * pool-1-thread-1	beforeInt:5	afterInt:6
     * pool-1-thread-1	beforeInt:6	afterInt:7
     * pool-1-thread-3	beforeInt:1	afterInt:2
     */
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    Integer beforeInt = myData.threadLocalField.get();
                    myData.add();
                    Integer afterInt = myData.threadLocalField.get();
                    System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\t" + "afterInt:" + afterInt);
                });
            }
        } finally {
            threadPool.shutdown();
        }
    }

}

class MyData {

    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);

    public void add() {
        threadLocalField.set(threadLocalField.get() + 1);
    }

}

public class ThreadLocalDemo2 {

    /**
     * 清理
     *
     * 运行结果:
     *
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-3	beforeInt:0	afterInt:1
     * pool-1-thread-1	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-2	beforeInt:0	afterInt:1
     * pool-1-thread-3	beforeInt:0	afterInt:1
     */
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\t" + "afterInt:" + afterInt);
                    } finally {
                        myData.threadLocalField.remove();
                    }
                });
            }
        } finally {
            threadPool.shutdown();
        }
    }

}

class MyData {

    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);

    public void add() {
        threadLocalField.set(threadLocalField.get() + 1);
    }

}

9.1.6 总结

  • 因为每个 Thread 内有自己的实例副本且该副本只有当前线程自己使用

  • 既然其他 ThreadLocal 不可访问,那就不存在多线程共享问题

  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的

  • 如何才能不争抢

    • 加入 synchronized 或者 lock 控制资源访问顺序

    • 人手一份,大家各自安好,没有必要争抢

9.2 ThreadLocal 源码分析

9.2.1 源码解读

  • Thread 和 ThreadLocal,人手一份

    public class Thread implements Runnable {
        
        ...
         
        ThreadLocal.ThreadLocalMap threadLocals = null;    
        
        ...
        
    }
    
  • ThreadLocal 和 ThreadLocalMap

    static class ThreadLocalMap {
        
        ...
    
            /**
             * The entries in this hash map extend WeakReference, using
             * its main ref field as the key (which is always a
             * ThreadLocal object).  Note that null keys (i.e. entry.get()
             * == null) mean that the key is no longer referenced, so the
             * entry can be expunged from table.  Such entries are referred to
             * as "stale entries" in the code that follows.
             */
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
        
        ...
    
    	public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
        /**
         * Variant of set() to establish initialValue. Used instead
         * of set() in case user has overridden the set() method.
         *
         * @return the initial value
         */
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
        
    	...
            
       	protected T initialValue() {
            return null;
        }
        
        ...
            
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
        
        ...
            
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
        
        ...
    
    }    
    

9.2.2 Thread、ThreadLocal、ThreadLocalMap关系

三者总概括

image

ThreadLocalMap 实际上就是一个 ThreadLocal 实例为 key,任意对象为 value 的 Entry 对象

void createMap(Thread t, firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

9.2.3 总结

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

image

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

9.3 内存泄漏问题

9.3.1 什么是内存泄漏

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

9.3.2 谁惹的祸?

再回首 ThreadLocalMap

static class ThreadLocalMap {
    
    ...

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
 
    ...
    
}

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

  1. 第一层包装是使用了 WeakReference<ThreadLoal<?>> 将 ThreadLocal 对象变成一个弱引用的对象

  2. 第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLoal<?>>

9.3.3 强软弱虚引用

image

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

强引用

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

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象

在 Java 中最常见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用

当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的

即使该对象以后永远都不会被用到,JVM 也不会回收,因此强引用时造成 Java 内存泄漏的主要原因之一

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

public class ReferenceDemo {

    /**
     * 运行结果:
     * gc before:com.my.tl.MyObject@74a14482
     * ----invoke finalize method
     * gc after:null
     */
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);
        myObject = null;
        //人工开启 GC,一般不用
        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc after:" + myObject);
    }

}

class MyObject {

    /**
     * 这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
     */
    @Override
    protected void finalize() throws Throwable {
        // finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作
        super.finalize();
        System.out.println("----invoke finalize method");
    }

}

软引用

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

对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,它会被回收

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

public class ReferenceDemo {

    /**
     * 修改运行内存 -Xms10m -Xmx10m
     *
     * 运行结果:
     * ----softReference:com.my.tl.MyObject@3f102e87
     * ----gc after 内存够用:com.my.tl.MyObject@3f102e87
     * ----gc after 内存不够用:null
     * ----invoke finalize method
     * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
     * 	at com.my.tl.ReferenceDemo.main(ReferenceDemo.java:19)
     */
    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("----softReference:" + softReference.get());
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----gc after 内存够用:" + softReference.get());
        try {
            byte[] bytes = new byte[1024 * 1024 * 20];//20MB 对象
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("----gc after 内存不够用:" + softReference.get());
        }

    }

    /**
     * 运行结果:
     * gc before:com.my.tl.MyObject@74a14482
     * ----invoke finalize method
     * gc after:null
     */
    private static void strongReference() {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);
        myObject = null;
        //人工开启 GC,一般不用
        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc after:" + myObject);
    }

}

class MyObject {

    /**
     * 这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
     */
    @Override
    protected void finalize() throws Throwable {
        // finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作
        super.finalize();
        System.out.println("----invoke finalize method");
    }

}

弱引用

弱引用需要用java.lang.ref.weakReference类来实现,它比软引用的生命周期更短

对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存空间

public class ReferenceDemo {

    /**
     * 运行结果:
     * ----gc before 内存够用:com.my.tl.MyObject@3abfe836
     * ----invoke finalize method
     * ----gc after 内存够用:null
     */
    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());
    }

    /**
     * 修改运行内存 -Xms10m -Xmx10m
     *
     * 运行结果:
     * ----softReference:com.my.tl.MyObject@3f102e87
     * ----gc after 内存够用:com.my.tl.MyObject@3f102e87
     * ----gc after 内存不够用:null
     * ----invoke finalize method
     * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
     * 	at com.my.tl.ReferenceDemo.main(ReferenceDemo.java:19)
     */
    private static void softReference() {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("----softReference:" + softReference.get());
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----gc after 内存够用:" + softReference.get());
        try {
            byte[] bytes = new byte[1024 * 1024 * 20];//20MB 对象
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("----gc after 内存不够用:" + softReference.get());
        }
    }

    /**
     * 运行结果:
     * gc before:com.my.tl.MyObject@74a14482
     * ----invoke finalize method
     * gc after:null
     */
    private static void strongReference() {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);
        myObject = null;
        //人工开启 GC,一般不用
        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc after:" + myObject);
    }

}

class MyObject {

    /**
     * 这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
     */
    @Override
    protected void finalize() throws Throwable {
        // finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作
        super.finalize();
        System.out.println("----invoke finalize method");
    }

}

软引用和弱引用的使用场景

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

  • 如果每次读取图片都从硬盘读取则会严重影响性能

  • 如果一次性全部加载到内存中又可能会造成内存溢出

此时使用软引用来解决这个问题

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

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>

虚引用

  1. 虚引用必须和引用队列(ReferenceQueue)联合使用

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

  2. PhantomReference 的 get 方法总是返回 null

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

  3. 处理监控通知使用

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

public class ReferenceDemo {

	/**
     * 修改运行内存 -Xms10m -Xmx10m
     * 
     * 运行结果:
     * null	list add ok
     * null	list add ok
     * null	list add ok
     * ----invoke finalize method
     * ----有虚对象回收加入了队列
     * java.lang.OutOfMemoryError: Java heap space
     * 	at com.my.tl.ReferenceDemo.lambda$phantomReference$0(ReferenceDemo.java:44)
     * 	at com.my.tl.ReferenceDemo$$Lambda$1/1854778591.run(Unknown Source)
     * 	at java.lang.Thread.run(Thread.java:748)
     */
    public static void main(String[] args) throws InterruptedException {
        MyObject myObject = new MyObject();
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);
        //System.out.println(phantomReference.get());

        List<byte[]> list = new ArrayList<>();
        new Thread(() -> {
            try {
                while (true) {
                    list.add(new byte[2 * 1024 * 1024]);
                    System.out.println(phantomReference.get() + "\t" + "list add ok");
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (OutOfMemoryError e) {
                e.printStackTrace();
                System.gc();
            }

        }, "t1").start();

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

    /**
     * 运行结果:
     * ----gc before 内存够用:com.my.tl.MyObject@3abfe836
     * ----invoke finalize method
     * ----gc after 内存够用:null
     */
    private 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());
    }

    /**
     * 修改运行内存 -Xms10m -Xmx10m
     * <p>
     * 运行结果:
     * ----softReference:com.my.tl.MyObject@3f102e87
     * ----gc after 内存够用:com.my.tl.MyObject@3f102e87
     * ----gc after 内存不够用:null
     * ----invoke finalize method
     * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
     * at com.my.tl.ReferenceDemo.main(ReferenceDemo.java:19)
     */
    private static void softReference() {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("----softReference:" + softReference.get());
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----gc after 内存够用:" + softReference.get());
        try {
            byte[] bytes = new byte[1024 * 1024 * 20];//20MB 对象
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("----gc after 内存不够用:" + softReference.get());
        }
    }

    /**
     * 运行结果:
     * gc before:com.my.tl.MyObject@74a14482
     * ----invoke finalize method
     * gc after:null
     */
    private static void strongReference() {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);
        myObject = null;
        //人工开启 GC,一般不用
        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc after:" + myObject);
    }

}

class MyObject {

    /**
     * 这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
     */
    @Override
    protected void finalize() throws Throwable {
        // finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作
        super.finalize();
        System.out.println("----invoke finalize method");
    }

}

9.3.4 关系

image

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 能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

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

public class T1 {
    
    volatile boolean flag;
    
    public static void main(String[] args) {
        ThreadLocal<String> tl = new ThreadLocal<>();//line1
        tl.set("xyz@123.com");//line2
        tl.get();//line3
    }
    
}

line1 新建一个 ThreadLocal 对象,t1 是强引用指向这个对象

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

image

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

若这个 key 引用是强引用,就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收,造成内存泄漏

若这个 key 引用是弱引用,就大概率会减少内存泄漏的问题(还有一个 key 为 nulll 的雷,第 2 个坑后面讲)

使用弱引用,就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null

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

  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 -> ThreadLocalMap -> Entry -> value 永远无法回收,造成内存泄漏

  2. 当然,如果当前 Thread 运行结束,ThreadLocal,ThreadLocalMap,Entry 没有引用链可达,在垃圾回收的时候都会被系统进行回收

  3. 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在 Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以 ThreadLocal 内存泄漏就值得我们小心

key 为 null 的 Entry,原理解析

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 对象后,手动调用 remove 方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果我们不手动调用 remove 方法,那么后面的线程就有可能获取到上个线程遗留下来的 value 值,造成 bug

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

expungeStaleEntry

清除脏 Entry,key 为 null 的 Entry

ThreadLocalMap get() 方法

public class ThreadLocal<T> {

    ...
    
	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //此处
            map.set(this, value);
        else
            createMap(t, value);
    }

	...
    
    static class ThreadLocalMap {

        ...


        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                //清除逻辑
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                //清除逻辑
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //具体的清除逻辑
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        ...

    }
 
    ...
    
}

ThreadLocalMap set() 方法

public class ThreadLocal<T> {
    
    ...
        
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //此处
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
 
    static class ThreadLocalMap {
        
        ...
        
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                //此处
                return getEntryAfterMiss(key, i, e);
    	}
    
		private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    //此处
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

		private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //此处
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        ...
        
    }
    
    
    ...
    
}

ThreadLocalMap remove() 方法

public class ThreadLocal<T> {
    
    ...

	public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             //此处
             m.remove(this);
    }

    static class ThreadLocalMap {
        
        ...
            
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    //此处
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
        
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //此处
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
        
        ...
        
    }
    
    
    ...
        
}

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

9.3.6 最佳实践

  • ThreadLocal 一定要初始化,避免空指针异常(ThreadLocal.withInitial(() -> 初始化值)

  • 建议把 ThreadLocal 修饰为 static

  • 用完记得手动 remove

9.4 小总结

  • ThreadLocal 并不解决线程间共享数据的问题

  • ThreadLocal 适用于变量在线程间隔且在方法间共享的场景

  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本,避免了实例线程安全问题

  • 每个线程持有一个只属于自己的专属 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题

  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题

  • 都会通过 expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

  • 群雄逐鹿起纷争,人各一份天下安

posted @ 2026-04-18 15:14  清风含薰  阅读(5)  评论(0)    收藏  举报