谈谈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:
谈谈ThreadLocal所引发的OOM_第1张图片
接着创建如下测试类,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> 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 insert() {
        List list = new ArrayList<>();
        for (int i = 0; i < DATA_SIZE; i++) {
            list.add(i);
        }
        return list;
    }
}

执行,在某个时段可能会看到如下结果,这里出现了OOM:
谈谈ThreadLocal所引发的OOM_第2张图片
因此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)。

你可能感兴趣的:(多线程)