一篇文章带你了解ThreadLocal

1 问题

学习了尚硅谷周阳老师的JUC课程,关于ThreadLocal输出一篇博客!

  • ThreadLocal是什么?具体应用场景?
  • Thread、ThreadLocal、ThreadLocalMap三者关系?
  • ThreadLocalMap的key为什么设计为弱引用?
  • ThreadLocal内存泄漏及解决?

2 概述

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

ThreadLocal实现了线程间的数据隔离,使每个线程都有专属的变量副本

  • T get():返回此线程局部变量副本中的值
  • void set(T value):将此线程局部变量的当前线程副本设置为指定值
  • protected T initialValue():返回此线程局部变量的当前线程的“初始值”
  • static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):创建一个线程局部变量
  • void remove():删除此线程局部变量的当前值
package tl;

import lombok.Getter;

import java.util.concurrent.CountDownLatch;

/**
 * 需求1: 5个销售员卖车,准确统计全部销售的总量
 * <p>
 * 需求2: 5个销售员卖车,记录每个销售的各自销量
 *
 * @author linxh
 * @date 2023/02/21
 */
public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        Car car = new Car();
        int length = 5;
        CountDownLatch countDownLatch = new CountDownLatch(length);
        for (int i = 0; i < length; i++) {
            new Thread(() -> {
                int size = (int) (Math.random() * 5 + 1);
                try {
                    for (int j = 0; j < size; j++) {
                        car.saleByLock();
                        countDownLatch.countDown();
                        car.saleBySelf();
                    }
                    System.out.println(Thread.currentThread().getName() + "单独卖出\t" + car.getThreadLocal().get());
                } finally {
                    car.getThreadLocal().remove();
                }
            }, "销售员" + (i + 1)).start();
        }
        countDownLatch.await();
        System.out.println("全部销售员合计卖出\t" + car.getCount());
    }
}

class Car {

    @Getter
    private Integer count = 0;

    public synchronized void saleByLock() {
        this.count++;
    }

    @Getter
    private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

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

3 源码分析

使用下面一段代码作为调试入口

public static void main(String[] args) {

    ThreadLocal<Integer> threadLocal1 = ThreadLocal.withInitial(() -> 0);
    ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "");
    ThreadLocal<Integer> threadLocal3 = ThreadLocal.withInitial(() -> 0);

    threadLocal1.set(1);
    threadLocal2.set("test");
    threadLocal3.set(3);

    System.out.println(threadLocal3.get());

    threadLocal1.remove();
    threadLocal2.remove();
    threadLocal3.remove();
}
  • public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) :该方法入参为供给型lambda表达式需提供一个初始值,SuppliedThreadLocal类型继承了ThreadLocal因此返回ThreadLocal对象,此方法仅是初始化ThreaLocal对象而已
  • 在main线程中创建了三个ThreadLocal对象,那么这三个ThreadLocal对象有和当前线程有什么关联呢?上面说了withInitial方法仅是初始化了ThreadLocal对象并没关联上任何线程。那直接查看set方法源码可以看到,是从当前线程即main线程中获取了一个ThreadLocalMap对象,此时查看Thread类源码发现ThreadLocalMap是Thread的实例变量,而ThreadLocalMap又是ThreadLocal的内部类
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • 如下调试结果,当前线程t为main,在执行完第三次set方法查看main线程中的实例变量ThreadLocalMap的table数据,threadLocal1的值1存放在数组下标1,threadLocal2的值“test”存放在数组下表8,threadLocal3的值3存放在数组下表15。此时估计大家都很清楚ThreadLocalMap的内部结构了!

在这里插入图片描述

  • 查看ThreadLocal.ThreadLocalMap源码可以发现
    • 类似HashMap内部维护了一个键值对Entry对象数组table
    • Entry对象的key为ThreadLocal,value为我们设置的值
    • ThreadLocal为弱引用
  • 因此可以得出调用get()/set()方法的时候是通过key(ThreadLoccal对象)进行自定义hash算法得到对应的table下标进行访问的(具体哈希算法就不分析了)
static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    // ...
            private Entry[] table;
    // ...
}
  • 当ThreadLocalMap对象为null时会进入else语句块中调用createMap方法直接创建Entry对象(没有好说的),但是当ThreadLocalMap对象不为null的时候会调用该对象的set方法进行赋值,下面截取set方法一部分核心源码进行分析
private void set(ThreadLocal<?> key, Object value) {
    // table循环遍历不为null的Entry对象,目的是查看这些Entry是否存在和入参ThreadLocal对象相等的key(做替换),key为null的Entry(做脏Entry的替换)
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取当前Entry的ThreadLocal对象
        ThreadLocal<?> k = e.get();
        // key重复替换value,并结束当前方法
        if (k == key) {
            e.value = value;
            return;
        }
		// key为null替换脏Entry,并结束当前方法
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 上面遍历的table下标的Entry,既不存在key为null的Entry,也不存在重复的ThreadLocal对,就直接新增一个Entry并清理一些key为null的table插槽,也是做脏Entry清理,即当前方法中使用replaceStaleEntry和cleanSomeSlots方法清理ThreadLocalMap中的脏Entry
    tab[i] = new Entry(key, value);
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  • 查看replaceStaleEntry和cleanSomeSlots方法清除脏Entry都是调用expungeStaleEntry方法进行清除的
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
	// 将当前下标Entry的value和Entry都置为null
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 容量-1(非数组长度,是table中的元素总个数)
    size--;
	// ...
    // 上面得到空key的Entry索引,下面将通过重新散列(哈希)位于这个索引到下一个key为null的Entry之间的任何哈希碰撞(else代码块),还会删除尾随key为null的脏Entry(if代码块)
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 清除staleSlot索引后面的空key脏Entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // hash冲突处理,将发生hash冲突的Entry前面的索引Entry置为null,找到一个可用索引槽位存放冲突的Entry。
                //个人理解:不同的hash算法都需要解决hash冲突,类似HashMap解决冲突使用的是链表和红黑树,而ThreadLocalMap则是直接使用前面的Entry替换了后面的Entry,并清理前面Entry的槽位
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
  • 类似的get方法源码的调试就不演示了,remove方法的源码就是上面expungeStaleEntry而已

4 开发规约

在这里插入图片描述

package tl;

import lombok.Getter;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 阿里规约明确指出必须回收自定义的ThreadLocal变量
 *
 * @author linxh
 * @date 2023/02/21
 */
public class ThreadLocalDemo2 {

    @Getter
    private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

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

    public static void main(String[] args) {
        ThreadLocalDemo2 demo = new ThreadLocalDemo2();

        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(3, 5, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
        try {
            for (int i = 0; i < 10; i++) {
                threadPoolExecutor.submit(() -> {
                    Integer beforeValue = demo.getThreadLocal().get();
                    demo.add();
                    Integer afterValue = demo.getThreadLocal().get();
                    System.out.println(Thread.currentThread().getName() + "\tbeforeValue:" + beforeValue + "\tafterValue:" + afterValue);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }

}
pool-1-thread-1	beforeValue:0	afterValue:1
pool-1-thread-4	beforeValue:0	afterValue:1
// 由于上面的线程1使用完局部变量未清理,导致再次复用线程1初始值错误,需要在finally块里手动调用remove()方法
// 第二个问题是有可能存在内存泄漏的问题
pool-1-thread-1	beforeValue:1	afterValue:2  
pool-1-thread-5	beforeValue:0	afterValue:1
pool-1-thread-3	beforeValue:0	afterValue:1
pool-1-thread-2	beforeValue:0	afterValue:1
pool-1-thread-3	beforeValue:1	afterValue:2
pool-1-thread-5	beforeValue:1	afterValue:2
pool-1-thread-1	beforeValue:2	afterValue:3
pool-1-thread-4	beforeValue:1	afterValue:2

5 Java引用及ThreadLocal内存泄漏

1)强软弱虚
  • 强引用

    • 当内存不足,JVM开始垃圾回收,对于强引用的对象就算时出现了OOM也不会对该对象进行回收。
    • Java中最常见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用
    • 当一个对象被强引用变量引用时,它处于可达状态此时时不会被垃圾回收机制回收的
    • 对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将相应的引用赋值为null,一般认为就是可以被垃圾收集(具体看虚拟机的垃圾回收策略)
  • 软引用

    • 软引用时一种相对于强引用弱化了的引用,需要用java.lang.ref.SoftReference类来实现
    • 当系统内存充足时弱引用的对象不回被回收,当系统内存不足时弱引用对象会被回收
    • 软引用通常用于在对内存敏感的程序中,类似高速缓存的实现可使用弱引用:内存足够时候就保留,不够就回收
  • 弱引用

    • 弱引用需要用java.lang.ref.WeakReference类来实现
    • 对于弱引用对象,只要JVM虚拟机只要执行垃圾回收,不管JVM内存是否足够都会回收该弱引用对象
  • 虚引用

    • 虚引用需要java.lang.ref.PhantomReference类来实现
    • 虚引用必须和引用队列(ReferenceQueue)联合使用
    • 如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾收集器回收
    • PhantomReferen的get方法总是返回null,因此无法访问对应的引用对象
    • 虚引用的主要作用时跟踪对象被垃圾回收的状态,仅仅时提供了一种确保对象被finalize以后,做某些事情的通知机制,用来实现比finalize机制更灵活的回收操作
package tl;

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

class MyObject {

    /**
     * 对象GC前回调的方法
     */
    @Override
    protected void finalize() throws Throwable {
        System.out.println("MyObject执行清除!!!");
    }
}

/**
 * @author linxh
 * @date 2023/02/21
 */
public class ReferenceDemo {

    public static void main(String[] args) throws InterruptedException {
        //strongReference();
        //softReference();
        //weakReference();
        //phantomReference();
    }

    private static void weakReference() throws InterruptedException {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("gc before:\t" + weakReference.get());
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("gc after:\t" + weakReference.get());
    }

    /**
     * -Xms10m -Xmx10m
     */
    private static void softReference() throws InterruptedException {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("gc 内存够用:\t" + softReference.get());
        try {
            byte[] bytes = new byte[10 * 1024 * 1024];
        } finally {
            System.out.println("gc 内存不够用:\t" + softReference.get());
        }
    }

    private static void strongReference() throws InterruptedException {
        MyObject myObject = new MyObject();
        System.out.println("gc before:\t" + myObject);
        // 根可达算法,只要引用一直不断开,GC就不会回收,手动断开
        myObject = null;
        // 人工开启GC
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("gc after:\t" + myObject);
    }
    
     /** 虚引用场景不好复现,没有demo */
    private static void phantomReference() {}
}
2)内存泄漏原因

有了上面源码分析和Java引用的基础下面就分析一下ThreadLocal内存泄漏问题

当我们创建ThreadLocal对象,实际上就是往当前Thread对象的ThreadLocal.ThreadLocalMap实例变量存放一个Entry。而Entry的key是当前ThreadLocal对象且为弱引用,当methodA方法栈执行完/当前线程结束,这个ThreadLocal的强引用被指为null,根据可达性分析这个ThreadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal实例就会被垃圾收集器回收(Hospot)

如果:当前thread运行结束,ThreadLocal、ThreadLocalMap、Entry对象均没有引用链可达,都会收垃圾收集器回收

但是:实际开发中,methodA方法栈出栈后强引用置为null且k是弱引用,这时候ThradLocal对象势必被垃圾收集器回收,但此时value不为null。如果我们使用的是线程池去维护我们的线程,这时候的线程是可复用且不会结束的,就会导致一种情况是:ThreadLocalMap中出现key为null,value不为null的Entry,且我们没有办法访问这些Entry(key=null)中的value,由于这个线程结束不了这些key为null的Entry中的value就会一直存在一条强引用链:Thread Ref =》Thread =》ThreadLocalMap =》Entry =》value,那么这个value就永远无法回收造成内存泄漏

在这里插入图片描述

3)解决方法

阿里规约强制要求必须回收自定义的ThreadLocal变量,因此在finally块中手动调用ThreadLocal对象的remove()方法,此外当我们调用get()和set()方法的时候也会帮我们清理脏Entry(key = null),具体清理脏Entry的三个方法为:expungeStaleEntry、replaceStaleEntry、cleanSomeSlots

  • 调用set()方法时,采样清理、全量清理,扩容时还会继续检查
  • 调用get()方法,没有直接命中,向后查找时会清理
  • 调用remove()时,除了清理当前Entry,还会向后继续清理和解决碰撞
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值