【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例

目录

1. ThreadLocal
2. ThreadLocal使用示例
3. ThreadLocal源码分析
4. 线程池使用ThreadLocal导致的内存泄露案例解析

1. ThreadLocal

ThreadLock是JDK包提供的,它提供了线程本地变量,也就是如果创建了一个ThreadLocal变量,访问这个变量的每个线程都会有这个变量的一个本地副本。
当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLock变量后,每个线程都会复制一个变量到自己的本地内存

ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联。

ThreadLocal定义了四个常用方法:
【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第1张图片

  • get() : 返回此线程局部变量的当前线程副本的值
  • initialValue(): 返回此线程局部变量的当前线程的"初始值"
  • remove(): 移除此线程局部变量当前线程的值
  • set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值

除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。

  1. ThreadLocal实例本身不存储值,它只是提供了一个在当前线程中找到副本值的key
  2. ThreadLocal包含在Thread中

1. ThreadLocal使用示例

public class ThreadLocalTest {
     

	// print函数
	static void print(String str) {
     
		// 1.1 打印当前线程本地内存中的localVariable变量的值
		System.out.println(str + ":" + localVariable.get());

		// 1.2 清除当前线程本地内存中的localVariable变量
//		localVariable.remove();
	}

	// 创建ThreadLocal变量
	private static ThreadLocal<String> localVariable = new ThreadLocal<>();

	public static void main(String[] args) {
     
		// 创建线程one
		Thread threadOne = new Thread(new Runnable() {
     
			@Override
			public void run() {
     
				// 设置线程one中本地变量的localVariable的值
				localVariable.set("threadOne local variable");
				// 调用打印函数
				print("threadOne");
				// 打印本地变量值
				System.out.println("threadOne remove after" + ":" + localVariable.get());
			}
		});

		// 创建线程two
		Thread threadTwo = new Thread(new Runnable() {
     
			@Override
			public void run() {
     
				// 设置线程two中本地变量的localVariable的值
				localVariable.set("threadTwo local variable");
				// 调用打印函数
				print("threadOne");
				// 打印本地变量值
				System.out.println("threadTwo remove after" + ":" + localVariable.get());
			}
		});

		// 启动线程
		threadOne.start();
		threadTwo.start();
	}
}

【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第2张图片

3. ThreadLocal源码分析

ThreadLocal只是一个工具类,具体存放变量的是线程的threadLocals变量。threadLocals是一个ThreadLocalMap类型的变量
【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第3张图片
由图可知,ThreadLocal内部是一个Entry数组,Entry继承自WeakReference,Entry内部的value用来存放通过ThreadLocal的set方法传递的值,ThreadLocal存放ThreadLocalMap的key中,如下所示

1. localVariable.set("threadOne local variable"); // 跟踪set(在ThredLocal中)方法

2. public void set(T value) {
     
        Thread t = Thread.currentThread();
         // 得到的是当前线程里面的变量ThreadLocalMap,存放key(ThreadLocal),value(val)的键值对
        ThreadLocalMap map = getMap(t);
        if (map != null)
        // 设置map键值对,this->ThreadLocal
            map.set(this, value); // 继续跟踪
        else
            createMap(t, value);
    }
3. private void set(ThreadLocal<?> key, Object value) {
     

            Entry[] tab = table; // 计算当前ThreadLocal变量所在的table数组,尝试使用快速定位方法
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i]; // 使用循环防止快速定位失效,遍历table数组
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
     
                ThreadLocal<?> k = e.get();

                if (k == key) {
     
                    e.value = value;
                    return;
                }

                if (k == null) {
     
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value); // 继续跟踪这个~~!
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
4. static class Entry extends WeakReference<ThreadLocal<?>> {
     
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
     
                super(k); // 继续跟踪到父类构造方法
                value = v;
            }
	}

5.	public WeakReference(T referent) {
     
  	          super(referent);
	}

6. Reference(T referent) {
     
            this(referent, null);
   }

k被传递给WeakReference的构造函数,也就是说ThreadLocalMap里面的key为ThreadLocal对象的弱引用,具体就是referent变量引用了ThreadLocal对象,value为具体调用ThreadLocal的set方法时传递的值

  • 每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,弱引用有利于GC回收。如果当前线程一直存在且没有调用ThreadLocal的remove方法,并且这时候其它地方还有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在对ThreadLocal变量的引用和对value对象的引用,它们是不会被释放的,这就会造成内存泄露
  • 即使设置ThreadLocal为null,还是会发生内存泄露,
    当ThreadLocal的key == null时,GC会回收这部分空间,但是value却不一定能够被回收

【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第4张图片
如何避免内存泄露?
在使用完毕后及时调用remove方法

4. 线程池使用ThreadLocal导致的内存泄露案例解析

public class ThreadPoolTest {
     
	static class LocalVariable{
     
		private Long[] a = new Long[1024*1024];
	}

	// 核心线程数:5   最大线程数:5
	final static ThreadPoolExecutor poolExcutor = new ThreadPoolExecutor(5,5,1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());

	final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();

	public static void main(String[] args) throws InterruptedException {
     
		for (int i = 0; i < 50; i++) {
     
			poolExcutor.execute(new Runnable() {
     
				@Override
				public void run() {
     
					localVariable.set(new LocalVariable());

					System.out.println("use local variable");
				}
			});
			Thread.sleep(1000);
		}
		System.out.println("pool execute over");
	}
}

【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第5张图片

使用localVariable.remove()方法后
【并发编程笔记】 ---- ThreadLocal原理及线程池内存泄露案例_第6张图片
对于两图,可知代码一发生了内存泄露

原因: 第一次运行代码时,在设置线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池的5个核心线程的threadLocals变量里面的new LocalVariable()实例没有被释放。虽然线程池里面的任务执行完,但是线程池里面的5个线程会一直存在知道JVM进程被杀死。
第二次运行代码时,及时调用了localVariable.remove()方法进行了清除,所以不会存在内存泄露

你可能感兴趣的:(并发)