首页 > 编程语言 >java中的ThreadLocal

java中的ThreadLocal

时间:2024-01-25 21:57:54浏览次数:31  
标签:java ThreadLocalMap ThreadLocal 实例 线程 Key Entry

1. ThreadLocal 的基本使用

  • 在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量每个线程中都有独立值,不会出现一个线程读取变量时而被另一个线程修改的现象。ThreadLocal类通常被翻译为线程本地变量类或者线程局部变量类。

  • ThreadLocal是位于JDK的java.lang核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。线程本地变量可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作线程本地变量的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

  • ThreadLocal如何做到为每个线程存有一份独立的本地值呢?

    • 一个ThreadLocal实例可以形象地理解为一个Map(早期版本的ThreadLocal是这样设计的)。
    • 当工作线程Thread实例向本地变量保持某个值时,会以Key-Value对(即键值对)的形式保存ThreadLocal内部的Map中,其中Key为线程Thread实例Value待保存的值
    • 当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。

image

方法 说明
set(T value) 设置当前线程在线程本地变量实例中绑定的本地值
get() 获得当前线程在线程本地变量实例中绑定的本地值
remove() 移除当前线程在线程本地变量实例中绑定的本地值
public class ThreadLocalTest {
    @Data
    static class Foo {
        //实例总数
        static final AtomicInteger AMOUNT = new AtomicInteger(0);
        int index = 0;  //对象的编号
        int bar = 10; //对象的内容
        
        //构造器
        public Foo() {
            index = AMOUNT.incrementAndGet(); //总数增加,并且给对象的编号
        }

        @Override
        public String toString() {
            return index + "@Foo{bar=" + bar + '}';
        }
    }

    //定义线程本地变量
    private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();

    @Test
    public void testThreadLocal() throws InterruptedException {
        //获取自定义的混合线程池
        ThreadPoolExecutor threadPool = ThreadUtil.getMixedTargetThreadPool();
        //共5个线程
        for (int i = 0; i < 5; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //获取“线程本地变量”中当前线程所绑定的值
                    if (LOCAL_FOO.get() == null) {
                        //设置“线程本地变量”中当前线程所绑定的值
                        LOCAL_FOO.set(new Foo());
                    }

                    System.out.println("初始的本地值:" + LOCAL_FOO.get());
                    //每个线程执行10次
                    for (int i = 0; i < 10; i++) {
                        Foo foo = LOCAL_FOO.get();
                        foo.setBar(foo.getBar() + 1);
                        sleepMilliSeconds(10);
                    }
                    System.out.println("累加10次之后的本地值:" + LOCAL_FOO.get());
                    //删除“线程本地变量”中当前线程所绑定的值,对于线程池中的线程尤其重要
                    LOCAL_FOO.remove();
                }
            });
        }

        ThreadUtil.sleepMilliSeconds(Integer.MAX_VALUE);
    }

image

  • 通过输出的结果可以看出,在线程本地变量(LOCAL_FOO)中,每一个线程都绑定了一个独立的值(Foo对象),这些值对象是线程私有的,可以理解为线程的本地值,每一次操作都是在自己的同一个本地值上进行的,从线程本地值的index始终一致可以看出,每个线程操作的是自己线程私有的Foo对象。

  • 如果线程尚未在本地变量(如LOCAL_FOO)中绑定一个值,直接通过get()方法去获取本地值会获取到一个空值,此时可以通过调用set()方法设置一个值作为初始值,

     //获取“线程本地变量”中当前线程所绑定的值
    if (LOCAL_FOO.get() == null) {
        //设置“线程本地变量”中当前线程所绑定的值
        LOCAL_FOO.set(new Foo());
    }
    
    
  • 在当前线程尚未绑定值时,如果希望能从线程本地变量获取到初始值,而且也不想采用以上的判空后设值这种相对烦琐的方式,可以调用ThreadLocal.withInitial(.)静态工厂方法,在定义ThreadLocal对象时设置一个获取初始值的回调函数

    ThreadLocal<Foo> LOCAL_F00 = ThreadLocal.withInitial(()-> new Foo());
    
  • 线程尚未绑定值而直接从线程本地变量获取值时,将会取得回调函数被调用之后所返回的值。

2. ThreadLocal使用场景

  • ThreadLocal是解决线程安全问题一个较好方案,它通过为每个线程提供一个独立的本地值去解决并发访问的冲突问题。在很多情况下,使用ThreadLocal比直接使用同步机制(如synchronized)解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。
  • ThreadLocal使用场景大致可以分为以下两类:
    1. 线程隔离:
      • ThreadLocal的主要价值在于线程隔离ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。
      • 由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。
      • ThreadLocal线程隔离的常用案例为: 可以每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
      • 常见的ThreadLocal使用场景为数据库连接独享Session数据管理等。
      • 线程隔离场景中,使用ThreadLocal的典型案例为: 可以每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。
    2. 跨函数传递数据
      • 通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。
      • 由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。
      • 使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。
      • 跨函数传递数据场景中使用ThreadLocal的典型案例为: 可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。

2.1 使用 ThreadLocal 进行线程隔离

  • ThreadLocal线程隔离应用场景的典型应用为数据库连接独享。下面的代码来自Hibernate,代码中通过ThreadLocal进行数据库连接(Session)的线程本地化存储,主要的代码如下:
private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s=(Session)threadSession.get();
    try {
        if (s == null){
            s = getSessionFactory().openSession();
            threadSession.set(s):
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }    
    return s;
}

Hibernate对数据库连接进行了封装,一个Session代表一个数据库连接。

HibernategetSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory()).openSession()来创建一个Session,再将Session设置到ThreadLocal变量中,这个Session相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session。

一般来说,完成数据库操作之后程序会将Session关闭,从而节省数据库连接资源。如果Session的使用方式为共享而不是独占,在这种情况下,Session是多线程共享使用的,如果某个线程使用完成之后直接将Session关闭,其他线程在操作Session时就会报错。所以Hibernate通过ThreadLocal非常简单实现了数据库连接的安全使用。

2.2 使用 ThreadLocal 进行跨函数数据传递

  • ThreadLocal在跨函数数据传递应用场景的典型有很多:

    1. 用来传递请求过程中的用户ID。
    2. 用来传递请求过程中的用户会话(Session)
    3. 用来传递HTTP的用户请求实例 HttpRequest
    4. 其他需要在函数之间频繁传递的数据。
    public classSessionHolder {
        
        // session id,线程本地变量
        private static final Threadlocal<String> sidLocal = new ThreadLocal<>("sidlocal");
        // 用户信息,线程本地变量
        private static final Threadlocal<UserDTo> sessionUserLocal = 
            							new ThreadLocal<>("sessionUserLocal");
        //session,线程本地变量
        private static final ThreadLocal<HttpSession> sessionLocal = 
            							new ThreadLocal<>("sessionLocal");
        //省略其他
      
        // 保存session在线程本地变量中
        public static void setSession(HttpSession session) {
             sessionLocal.set(session);
        }
        
        // 取得绑定在线程本地变量中的session
        public static HttpSession getSession() {
            HttpSession session=sessionLocal.get();
            Assert.notNull(session,"session未设置");
            return session:
        }
        
        //省略其他
    }
    
    

3. ThreadLocal 内部结构演进

  • 在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在线程本地变量中绑定的值为Vaue(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal实例,每一个ThreadLocal实例拥有一个Map实例。

  • 在大部分的应用中,实际上线程比较多,往往会配置数百个线程。反过来说,一个应用的线程局部变量又很少,可能就几个。

  • HashMap在扩容时存在高成本、低性能问题。为什么呢?

    • HashMap的内部是一个槽位(slot)数组,这个数组也叫哈希表,存储的是Key哈希值。当槽位数组中的元素个数超过容量(默认为16) x 加载因子(默认为0.75)也就是是12的时候,槽位数组会进行扩容,扩容成32个槽位。
    • 对于每一个槽位,可以理解为一个桶(bucket),如果一个桶内元素超过8个,链表会转换成红黑树。无论是槽位数组扩容还是桶内链表转换成红黑树,这都是高成本、低性能的扩容工作。
  • ThreadLocal实例内部的Map结构叫作ThreadLocalMap,并没有直接采用HashMap对象,而是自定义的和HashMap类似的结构。与HashMap不同的是,ThreadLocalMap去掉了桶结构,如果发生哈希碰撞,将Key相同的Entry放在槽位后面相邻的空闲位置上。为了区分这两种处理碰撞的方案

    • HashMap(数组加链表)的处理方式叫作链地址法,即发生碰撞就把Entry放在桶的链表中。
    • ThreadLocalMap的处理方式叫作开放地址法,即发生碰撞,就按照某种方法继续探测哈希表中的其
      他存储单元,直到找到空位置为止。
  • ThreadLocalMapHashMap一样,槽位数组(哈希表)在扩容时存在高成本、低性能问题。其槽位数组初始的容量为16,当槽位数组中的元素个数超过容量(默认为16) x 加载因子(默认为0.75)也是12的时候,槽位数组会进行扩容,扩容成32个槽位。这里需要创建一个新的数组,再进行Entry的哈希值的二次取模,在新数组找到新的位置后放入。

    由于ThreadLocalMap扩容存在性能问题,因此在线程比较多、线程局部变量少的场景下是不是可以转换思路,将ThreadLocal实例变成Key,一个线程一个Map呢?这是可以的,而且免去了ThreadLocalMap高成本、低性能的扩容工作。

  • 在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例。另外,Map结构的Key值也发生了变化: 新的KeyThreadLocal实例。

  • 在JDK 8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap),如果我们给一个Thread创建多个ThreadLocal实例,然后放置本地数据,那么当前线程的ThreadLocalMap中就会有多个Key-Value对,其中ThreadLocal实例为Key,本地数据为Value

  • 新版本的ThreadLocalMap还是由ThreadLocal类维护的,由ThreadLocal负责ThreadLocalMap实例的获取和创建,并从中设置本地值、获取本地值。所以ThreadLocalMap还寄存于ThreadLocal内部,并没有被迁移到Thread内部。

image

  • 每一个线程在获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,自己也无法访问别人的ThreadLocalMap实例,达到相互隔离,互不干扰。

3.1 变化

  • 与早期版本的ThreadLocalMap实现相比,新版本的主要变化为:
    1. 拥有者发生了变化: 新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal
    2. Key发生了变化: 新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。
    3. 与早期版本的ThreadLocalMap实现相比,新版本的主要优势为:
      • 每个ThreadLocalMap存储的Key-Value对数量变少。早期版本的Key-Value对数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储Key-Value对数量也多。
      • 新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。
      • 早期版本ThreadLocalMap的拥有者为ThreadLocal,在Thread(线程)实例被销毁后,ThreadLocalMap还是存在的,新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例被销毁后,ThreadLocalMap也会随之被销毁,在一定程度上能减少内存的消耗。

4. ThreadLocal源码分析

4.1 set(T value) 方法

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // value被绑定到thradlocal上
        map.set(this, value);
    else
        // 不存在就创建
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

通过以上的源码可以看出set(T value)方法的执行流程,大致如下:

  1. 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
  2. 如果map不为空,就将Value设置到map中,当前的ThreadLocal作为key。
  3. 如果map为空,为该线程创建map,然后设置第一个Key-Value对,Key为当前的ThreadLocal实例,Value为set方法的参数value值。

4.2 get()方法

  • get()方法用于获取“线程本地变量”在当前线程的ThreadLocalMap中对应的值,相当于获取线
    程本地值,其核心源码如下:
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以当前ThreadLocal为Key,尝试获得条目
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 条目存在 直接获取
            T result = (T)e.value;
            return result;
        }
    }
    // 如果当前线程对应map不存在或者map存在,但是当前ThreadLocal实例没有对应的Key-Value对,返回初始值
    return setInitialValue();
}

// 设置ThreadLocal关联的初始值并返回
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;
}

通过以上的源码可以看出T get()方法的执行流程,大致如下:

1)先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。

2)如果获得的map不为空,那么以当前ThreadLocal实例为Key尝试获得map中的Entry(条目)

3)如果Entry条目不为空,就返回Entry中的Value

4)如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。如果map不存在,还会给当前线程创建新ThreadLocalMap成员,并绑定第一个Key-Value对

4.3 remove()方法

  • remove()方法用于在当前线程的ThreadLocalMap中移除线程本地变量所对应的值,其核心源码如下:
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

4.4 initialValue()方法

  • 线程本地变量在当前线程的ThreadLocalMap中尚未绑定值时,initialValue()方法用于获取初始值。
protected T initialValue() {
    return null;
}

  • 如果没有调用set()而直接调用get(),就会调用此方法,但是该方法只会被调用一次。

  • 默认情况initialValue()方法返回null,如果不想返回null,可以继承ThreadLocal以覆盖此方法

  • 真的需要继承ThreadLocal去重写initialValue()方法吗? 其实没有必要。

    • JDK已经定义了一个ThreadLocal的内部SuppliedThreadLocal静态子类
    • 提供了ThreadLocal.withInitial(..)静态工厂方法,方便大家在定义ThreadLocal实例时设置初始值回调函数。
     public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
            return new SuppliedThreadLocal<>(supplier);
     }
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
        private final Supplier<? extends T> supplier;
    
        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }
    
        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }
    
    

5. ThreadLocalMap 源码分析

5.1 ThreadLocalMap 的主要成员变量

public class ThreadLocal<T> {

    static class ThreadLocalMap {
        	// Map条目初始容量
            private static final int INITIAL_CAPACITY = 16;
        	// Map条目数组
            private Entry[] table;
        	// Map条目数量
            private int size = 0;
        	// 扩容因子
            private int threshold; // Default to 0
        	// Map条目类型,一个静态的内部类,Entry继承子WeakReference,Key为ThreadLocal实例
            static class Entry extends WeakReference<ThreadLocal<?>> {
                    Object value;
                    Entry(ThreadLocal<?> k, Object v) {
                        super(k);
                        value = v;
                    }
                }

ThreadLocal源码中get()set()remove()方法都涉及ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几个方法:

  1. set(ThreadLocal<?>key,Object value): 向Map实例设置Key-Value对
  2. getEntry(ThreadLocal): 从Map实例获取Key(ThreadLocal实例)所属的Entry。
  3. remove(ThreadLocal): 根据Key(ThreadLocal 实例)从Map实例移除所属的Entry。
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 根据 key 的 HashCode ,找到key在数组上的槽点i
    int i = key.threadLocalHashCode & (len-1);
	
    // 从槽点i开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
    // 如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		// 找到现有槽点: Key值为ThreadLocal实例
        if (k == key) {
            e.value = value;
            return;
        }
		// 找到异常槽点: 槽点被GC掉,重设Key值和Value值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 没有找到现有的槽点,增加新的Entry
    tab[i] = new Entry(key, value);
    // 设置ThreadLocal数量
    int sz = ++size;
    // 清理 Key 为 null 的无效Entry
    // 没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

5.2 Entry 的 Key 需要使用弱引用

  • Entry用于保存ThreadLocalMapKey-Value条目,但是Entry使用了对Threadlocal实例进行包装之后的弱引用(WeakReference)作为Key
// Entry继承了WeakReference,并使用WeakReference对Key进行包装
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k); // 使用WeakReference对Key值进行包装
        value = v;
    }
}

为什么Entry需要使用弱引用对Key进行包装,而不是直接使用Threadlocal实例作为Key呢。这里从一个简单的例子入手,假设有一个方法funcA()创建了一个线程本地变量

public void funcA(){
    //创建一个线程本地变量
    ThreadLocal local=new ThreadLocal<Integer>();
    //设置值
    local.set(100);
    //获取值
    local.get();
    //函数末尾
}

  • 线程tn执行funcA()方法到其末尾时,线程tn相关的JVM栈内存以及内部ThreadLocalMap成员的结构如图。

image

  • 线程tn调用funcA()方法新建了一个ThreadLocal实例,使用local局部变量指向这个实例,并且此local强引用

  • 在调用local.set(100)之后,线程tnThreadLocalMap成员内部会新建一个Entry实例,其Key以弱引用包装的方式指向ThreadLocal实例。

  • 线程tn执行完funcA()方法后,funcA()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的EntryKey引用还指向了ThreadLocal实例。如果EntryKey引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏。

image

什么是弱引用? 弱引用(WeakReference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。

  • 由于ThreadLocalMapEntryKey使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被EntryKey所指向的ThreadLocal实例能被顺利回收。
  • EntryKey引用被回收之后,其EntryKey值变为null。后续当ThreadLocalget()set()remove()被调用时,ThreadLocalMap的内部代码会清除这些KeynullEntry,从而完成相应的内存释放。

使用ThreadLocal会发生内存泄漏的前提条件如下

  1. 线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。
  2. ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他ThreadLocal实例的get()set()remove()操作。只要存在一个针对任何ThreadLocal实例的get()set()remove()操作,就会触发Thread实例拥有的ThreadLocalMapKeynullEntry清理工作,释放掉ThreadLocal弱引用为null的Entry。

综合以上两点可以看出: 使用ThreadLocal出现内存泄漏还是比较容易的。

5.3 避免ThreadLocal内存泄漏

  • 编程规范推荐使用 static final 修饰 ThreadLocal 对象,ThreadLocal实例作为ThreadLocalMapKey,针对一个线程内所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。

  • 为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更

    // 推荐使用static final线程本地变量
    private statie final ThreadLocal<Foo> LOCAL_F00 = new ThreadLocal<Foo>();
    
  • 使用staticfinal修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMapEntryKeyThread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完staticfinal修饰ThreadLocal实例,使用完后必须使用remove()进行手动释放。

  • 如果使用线程池,可以定制线程池的afterExecute()方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用ThreadLocal实例的remove()方法对其手动释放,从而使得其线程内部的Entry得到释放

// 线程本地变量,用于记录线程异步任务的开始执行时间
private static final Threadlocal<Long> START_TIME= new Threadlocal<>();
ExecutorService pool = new ThreadPoolExecutor(
    2,4, 60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(2)){
        // 异步任务执行完成之后的钩子方法
        @Override
        protected void afterExecute(Runnable target, Throwable t) {
          	// 清空ThreadLocal实例的本地值
            START_TIME.remove();
        }
};

6. ThreadLocal注意事项

由于ThreadLocal使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用ThreadLocal时遵守以下两个原则:

  1. 尽量使用private static final修饰ThreadLocal实例。使用 private与final修饰符主要是尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要为了确保ThreadLocal实例的全局唯一。
  2. ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏的方法。

标签:java,ThreadLocalMap,ThreadLocal,实例,线程,Key,Entry
From: https://www.cnblogs.com/ccblblog/p/17988258

相关文章

  • Java方法详解
    Java方法详解1、何谓方法System.out.println(),那么它是什么呢?Java方法是语句的集合,他们在一起执行以一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法的程序中被创建,在其他地方被引用设计方法的原则:方法的本意是功能块,就是实现某个功......
  • 使用 JavaScript 宏删除文档中的特定注释
    有时只需要删除文档中的注释,要怎么快速做到呢?在这篇文章中,我们将会展示如何为ONLYOFFICE创建一个简单的宏,来删除某些特定的或所有评论,从而保持协作的重点和整洁。什么是ONLYOFFICE 宏如果您是一名资深MicrosoftExcel用户,那么相信您已对于VBA宏非常熟悉了。这些宏是帮助您自......
  • 了解Java事务管理
    在软件开发过程中,事务管理是一个非常重要的概念.事务用于确保数据库操作的一致性和完整性,并且具有原子性、一致性、隔离性和持久性的特性.Java提供了强大的事务管理机制,使得我们能够更好地处理数据的一致性和并发控制.Java事务管理主要通过JavaTransactionAPI(JTA)和Java......
  • Java学习日记 Day11
    Maven:把maven课程速通了,比较简单,其实就是对工程框架的一个配置,可以用一个总pom文件让整个工程的版本得到确定。SpringMVC:是Servlet的plus版,今天开了个头,明天继续学。算法:①二叉树的所有路径:递归加回溯,用一个List储存结果,一个双向队列储存路径。如果没遇到叶子节点就继续向里递......
  • java初学者
    day2packagebase;publicclassTest05{publicstaticvoidmain(String[]args){inti=128;byteb=(byte)i;//强制转换(类型)变量名System.out.println(i);System.out.println(b);bytea=12;intc......
  • 2024年1月Java项目开发指南9:密码加密存储
    提前声明:你不会写这加密算法没关系啊,你会用就行。要求就是:你可以不会写这个加密算法,但是你要知道加密流程,你要会用。@ServicepublicclassPasswordEncryptor{}很好,请在service层中创建一个名字为PasswordEncryptor的服务类,用来负责密码的加密。加密的方法有很多。简单一......
  • java系统与文件操作
    1.目录文件操作创建File对象,后续操作皆基于File,而不是String路径importjava.io.File;importjava.io.FilenameFilter;Filedir=newFile("C:\\Users\\Desktop");//目录Filefile=newFile("C:\\Users\\Desktop\\text.docx");//文件Filedir_......
  • java收发邮件
    邮箱协议端口使用jakarta库发送邮件示例importcom.alibaba.fastjson2.JSON;importcom.alibaba.fastjson2.JSONObject;importcom.xin_admin.common.Result;importcom.xin_admin.security.AuthAnnotation;importjakarta.activation.DataHandler;importjakarta.act......
  • 2024年1月Java项目开发指南8:统一数据返回格式
    有时候返回一个字符串,有时候返回一串数字代码,有时候返回一个对象……不过怎么说,我们返回的内容往往具有三个1.消息代码code2.消息内容msg3.数据内容data接下来,我们要编写一个类,通过这个类,实现对所有返回内容进行格式化。先去添加个依赖 <dependency> <groupId>org.p......
  • 笨办法学 Java(四)
    原文:LearnJavaTheHardWay译者:飞龙协议:CCBY-NC-SA4.0练习55:记录数组记录很棒,数组更好,但是当你把记录放入数组时,这个生活中几乎没有你不能编码的东西。1classStudent2{3Stringname;4intcredits;5doublegpa;6}78publicclass......