ThreadLocal存储结构及内存溢出问题分析

ThreadLocal是为了解决多线程并发访问共享变量时造成数据异常的问题,与加锁的思想方式不同,ThreadLocal是通过为每个线程提供一个变量的副本,以此保证并发访问的安全。

先看一下在没有使用ThreadLocal的情况下对于共享变量的访问结果:

/**
 * 启动两个线程,各执行100次对共享变量count加1,得到的结果可能并不是200,而是一个无法确定的数
 */
public class NoUseThreadLocal {
    static int count = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
            }
            System.out.println("count: " + count);
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
            }
            System.out.println("count: " + count);
        }).start();
    }
}

我们都知道这样的结果是无法确定的。

count: 199
count: 199

使用ThreadLocal的方式。

public class UseThreadLocal {
    private static ThreadLocal<Integer> threadLocal
            = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                threadLocal.set(threadLocal.get() + 1);
            }
            System.out.println("count: " + threadLocal.get());
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                threadLocal.set(threadLocal.get() + 1);
            }
            System.out.println("count: " + threadLocal.get());
        }).start();
    }
}

无论执行多少次得到的结果都是两个100。

count: 100
count: 100

通过这个例子就说明了threadlocal可以保证多个线程对共享变量的访问安全,并且由结果也可以看出,每个线程是自己玩自己的,也就是线程内部使用变量副本的方式,所以得到的结果是100而不是200。

ThreadLocal部分关键源码

public void set(T value) {
	//获取当前线程
    Thread t = Thread.currentThread();
    //从当前线程中获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//放入Entry数组中
        map.set(this, value);
    else
        createMap(t, value);
}

//ThreadLocal中的一个静态内部类,线程对象持有这样一个引用
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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);
}


public T get() {
    Thread t = Thread.currentThread();
    //拿到当前线程的ThreadLocalMap属性
    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();
}

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);
}

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

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

Thread对象中有一个ThreadLocalMap的属性

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal存储结构如下图:
ThreadLocal存储结构及内存溢出问题分析_第1张图片
线程对象中有一个ThreadLocalMap属性,ThreadLocalMap中又有一个Entry数组对象,当调用get或者set方法时都是先找到当前线程的ThreadLocalMap属性,然后再通过ThreadLocalMap获取Entry数组对象,数据就是由Entry负责存储的,Entry中的key实际上是通过弱引用的方式引用了ThreadLocal的实例。

ThreadLocal内存泄露分析

根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,ThreadLocalMap内部又维护一个Entry,Entry的 key 是 ThreadLocal实例本身,value 是真正需要存储的数据,而key与ThreadLocal之间又是一种弱引用关系。

先看一张线程对象和ThreadLocal对象的引用关系图。
ThreadLocal存储结构及内存溢出问题分析_第2张图片
当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,只有一条与key关联的弱引用路径,所以threadlocal将在下一次gc时被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,而这些key又是无法被访问到的,所以如果当前线程一直存活的话,就会存在这样一条强引用链:CurrentThreadRef -> CurrentThread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,数据也一直存在于内存中,内存泄露的问题就产生了。
然而大多数我们使用线程的场景都是通过线程池来管理,而线程池刚好是不会真正销毁线程的。

为什么要使用弱引用?
首先明确一点,内存泄露的问题和弱引用没有任何关系,使用弱引用的原因正是为了方便对象的回收和避免内存泄露,假设threadlocal的实例与key之间是强引用的关系,那么即使threadlocal自身的引用断了以后也不能对threadlocal进行回收,因为threadlocal还和key存在着强引用的关系。

所以如果不使用弱引用,那么当我们threadlocal使用完之后,必须手动清除threadlocal与key之间的引用关系,否则造成的是整个Entry对象级别的内存泄露。

如何避免内存泄露?
现在我们已经知道造成内存泄露的根本原因是因为当前线程引用着Entry对象,导致Entry对象不能被回收,从而导致value中的数据占用着内存空间。

其实要解决此问题,只需要我们使用完之后再手动调用remove方法即可。

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

ThreadLocal内存溢出代码演示

public class ThreadLocalDemo {

    final static ThreadPoolExecutor threadPoolExecutor
            = new ThreadPoolExecutor(10, 10,
            1,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>());

    static class MyData{
    	//构建对象就会占用1M的内存
        private byte[] a = new byte[1024 * 1024 * 1];
    }

    ThreadLocal<MyData> threadLocal;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; ++i) {
            threadPoolExecutor.execute(() -> {
                ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
                threadLocalDemo.threadLocal = new ThreadLocal<>();
                threadLocalDemo.threadLocal.set(new MyData());
                //threadLocalDemo.threadLocal.remove();
            });
            Thread.sleep(100);
        }
    }
}

堆内存的使用量呈明显的上升趋势
ThreadLocal存储结构及内存溢出问题分析_第3张图片

把最后的remove方法加上之后,可以看出堆内存的使用量明显降低了。

ThreadLocal存储结构及内存溢出问题分析_第4张图片

你可能感兴趣的:(并发编程,java,多线程)