谈谈ThreadLocal所引发的OOM

一、ThreadLocal简介

ThreadLocal常用于多线程环境下保证线程安全,它和synchronized以及lock略有不同,后两者是通过对访问权限的控制保证线程安全,而前者是通过增多资源的副本,保证当前线程操作自己所持有的副本,而不需要去与其它线程竞争。

二、ThreadLocal内部数据的存储

ThreadLocal将当前线程以及它操作的变量存储于一个ThreadLocalMap结构中,可以在ThreadLocal的set()方法中找到ThreadLocalMap,如下:

 public void set(T value) {
        Thread t = Thread.currentThread();//获取线程对象
        ThreadLocalMap map = getMap(t);//查看当前线程是否已存在ThreadLocalMap 
        if (map != null)
            map.set(this, value);//存在,值覆盖
        else
            createMap(t, value);//否则,创建一个当前线程的ThreadLocalMap 
    }

观察ThreadLocalMap的结构可以发现,它的key是WeakReference的即弱引用,而value是强引用。为什么要把key设置为弱引用呢?因为,由于ThreadLocalMap的生命周期和线程的生命周期相同,如果key设置为强引用的话,即使ThreadLocal执行结束,但只要线程周期未止,那么ThreadLocalMap对象将永远不会被回收,这样尤其容易发生内存溢出的问题。

三、弱引用的问题

是否使用了弱引用类型的key,ThreadLocal就可以完全避免OutOfMemoryError?下面做一个测试,首先修改虚拟机的最大堆空间为200M:
heapsize
接着创建如下测试类,ThreadLocalOOM:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalOOM {
    private static final int THREAD_SIZE = 3000;//线程池大小
    private static final int DATA_SIZE = 2000;//每个线程对应的变量所存储数据的数量
    private static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(THREAD_SIZE);
        for (int i = 0; i < THREAD_SIZE; i++) {
            service.execute(() -> {
                threadLocal.set(new ThreadLocalOOM().insert());
                System.out.println(Thread.currentThread().getName());
            });
        }
    }

    private List<Integer> insert() {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < DATA_SIZE; i++) {
            list.add(i);
        }
        return list;
    }
}

执行,在某个时段可能会看到如下结果,这里出现了OOM:
在这里插入图片描述
因此ThreadLocal并不能够完全避免OOM异常,那么它是怎么做的去减少OOM出现的概率呢?可以看到在ThreadLocal里面有一个remove()方法,如下:

     public void remove() {
           ThreadLocalMap m = getMap(Thread.currentThread());//如果当前线程有对应的ThreadLocalMap
           if (m != null)
               m.remove(this);//将会调用下面的方法
       }
    
      /**
         * Remove the entry for key.
         */
         //找到key对应的ThreadLocalMap,并清除entry数据
        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;
                }
            }
        }

这样ThreadLocalMap中所持有的value也被释放掉,而key则会随着GC清除,避免了OOM的出现。

四、总结

通过上面得测试可知,当使用ThreadLocal的时候,应当显示的在适当的时候调用其remove方法,以避免虚拟机爆出OOM,除此之外,每一个ThreadLocalMap之中不应该存储太大的数据(防止某些必须与线程同生命周期的对象无法得到清除,导致OOM)。

ThreadLocal 是 Java 中的一个类,用于解决多线程并发访问时的线程安全问题。它提供了一种线程级别的变量隔离机制,使得每个线程都拥有自己独立的变量副本,从而避免了多线程之间的数据竞争和冲突。 在多线程环境下,如果多个线程共享同一个变量,并且对该变量进行读写操作,就有可能引发并发安全问题,比如数据被意外修改、读取到脏数据等。为了解决这个问题,可以使用 ThreadLocalThreadLocal 使用一个特殊的 Map 结构来存储每个线程的变量副本,其中键为当前线程对象,值为对应的变量副本。每个线程都可以通过 ThreadLocal 对象获取自己的变量副本,并对其进行读写操作,而不会影响其他线程的变量副本。 具体实现时,可以通过 ThreadLocal 的静态方法 `set()` 和 `get()` 来设置和获取当前线程的变量副本。每个线程都可以通过 `get()` 方法获取自己的变量副本,如果变量副本不存在,则可以通过 `set()` 方法创建并初始化一个新的副本。每个线程都是独立操作自己的变量副本,互不干扰。 因为每个线程都拥有自己的变量副本,所以不需要进行同步机制(如锁)来保护变量的访问,从而提高了并发性能。但需要注意的是,虽然解决了线程安全问题,但使用不当或过度使用 ThreadLocal 也可能导致内存泄漏和上下文切换开销增加的问题,因此在使用时需要谨慎权衡。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值