探究ThreadLocal和ThreadPoolExecutor中的内存泄露风险与防范策略

探究ThreadLocal和ThreadPoolExecutor中的内存泄露风险与防范策略

本文将探讨ThreadLocal和ThreadPoolExecutor中可能存在的内存泄露问题,并提出相应的防范策略。

ThreadPoolExecutor的内存泄露问题

ThreadPoolExecutor是一个线程池类,它可以管理和复用线程,从而提高程序的性能和稳定性。但是,如果使用不当,ThreadPoolExecutor也会导致内存泄露问题。

首先来说,如果我们在使用ThreadPoolExecutor时没有正确地关闭线程池,就会导致线程一直存在,从而占用大量的内存。为了避免这种情况的发生,我们可以在程序结束时手动关闭线程池。具体来说,我们可以在finally块中调用shutdown方法,从而确保线程池一定会被关闭。

ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue);
try {
    // do something
} finally {
    executor.shutdown();
}

不过在实际生产过程中,大多数时候并不能关闭线程池,为了在无法关闭线程池的运行生产环境中防止内存泄漏

  1. 限制线程池的大小:确保线程池的大小适合工作负载和可用系统资源。过大的线程池可能导致过多的内存消耗,同时尽可能复用线程池,避免在项目中过多的创建线程池
  2. 使用有界队列:考虑使用有界队列(例如ArrayBlockingQueue)而不是无界队列。有界队列可以控制任务的排队数量,避免无限制的内存增长。
  3. 优化任务的执行时间:尽量减少任务的执行时间,避免长时间的任务阻塞线程池的线程。
  4. 定期监控线程池的状态:通过监控线程池的活动线程数、队列大小和任务执行情况,及时发现异常情况并进行调整。

ThreadLocal的内存泄露问题

ThreadLocal是一个多线程编程中常用的工具类,它允许我们在每个线程中存储和获取对象,而不必担心线程安全问题。但是,如果使用不当,ThreadLocal也会导致内存泄露问题。
通常情况下,我们会在使用完ThreadLocal后将其置为null,以便垃圾回收器可以回收它所占用的内存。但是,如果我们在某些情况下没有将其置为null,那么它就会一直占用内存,直到程序结束。
为了避免这种情况的发生,我们可以使用ThreadLocal的remove方法手动删除已经不再需要的变量。具体来说,我们可以在finally块中调用remove方法,从而确保变量一定会被删除。

ThreadLocal threadLocal = new ThreadLocal();
try {
    threadLocal.set(new Object());
    // do something
} finally {
    threadLocal.remove();
}

 
  

实际项目中在线程池中使用ThreadLocal导致内存溢出的案例,背景是在线程池中发送数据到kafka,并自定义了拒绝策略,在拒绝策略中把拒绝的相关信息打印出来。模拟相关业务代码

public class ThreadPoolUtil {
   private static ThreadLocal<ThreadLocalMemoryEntity>threadLocal= new ThreadLocal<>();
   //处理业务的线程池  核心参数设置小保证能进入拒绝策略
   private static final ThreadPoolExecutorcompensateBatchPool= new ThreadPoolExecutor(
         10, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue(1)
         , new LogPolicy());
   //模拟客户端发送请求的线程池
   private static final ThreadPoolExecutorsimulateReqPool= new ThreadPoolExecutor(
         1000, 1000, 1, TimeUnit.SECONDS, new ArrayBlockingQueue(1000));

   public static ThreadPoolExecutor getCompensateBatchPool() {
      returncompensateBatchPool;
   }

   public static ThreadPoolExecutor getSimulateReqPool() {
      returnsimulateReqPool;
   }

   public static ThreadLocal<ThreadLocalMemoryEntity> getThreadLocal() {
      returnthreadLocal;
   }
   // 拒绝策略
   public static class LogPolicy implements RejectedExecutionHandler {
      @Override
      public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
         ThreadLocalMemoryEntity threadLocalMemoryEntity =threadLocal.get();
         // 模拟记录内容
         System.out.println("执行拒绝策略:"+ threadLocalMemoryEntity.getName());
      }
   }
}

业务实体

@Data
public class ThreadLocalMemoryEntity {
	private String name;

	private byte[] data = new byte[1024*1024];
}

idea配置堆内存空间 Xms512m -Xmx512m

业务代码

public class ThreadLocalMemoryLeakExample {
	private static ThreadLocal<ThreadLocalMemoryEntity> threadLocal = ThreadPoolUtil.getThreadLocal();
	private static ThreadPoolExecutor compensateBatchPool = ThreadPoolUtil.getCompensateBatchPool();
	private static ThreadPoolExecutor SimulateReqPool = ThreadPoolUtil.getSimulateReqPool();

	public static void main(String[] args) {
		try {
			for (int i = 0; i < 10000; i++) {
				ThreadLocalMemoryEntity threadLocalMemoryEntity = new ThreadLocalMemoryEntity();
				threadLocalMemoryEntity.setName("test" + i);
				//模拟发送请求 实际生产中每一个请求都会有一个线程
				SimulateReqPool.execute(() -> {
					threadLocal.set(threadLocalMemoryEntity);
					//模拟执行业务逻辑
					compensateBatchPool.execute(() -> {
						//模拟发送kafka消息
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println("发送kafka数据:" + threadLocalMemoryEntity.getName());
					});

				});
			}
		} finally {
			compensateBatchPool.shutdown();
			SimulateReqPool.shutdown();

		}

	}
}

直接运行mian方法,结果如下
探究ThreadLocal和ThreadPoolExecutor中的内存泄露风险与防范策略_第1张图片
虽然GC会不断回收new的ThreadLocalMemoryEntity对象,但是由于不断将ThreadLocalMemoryEntity放入ThreadLocal中,导致内存溢出异常抛出。
如何防范ThreadLocal内存溢出
优化后的业务代码

public class ThreadLocalMemoryLeakExample {
	private static ThreadLocal<ThreadLocalMemoryEntity> threadLocal = ThreadPoolUtil.getThreadLocal();
	private static ThreadPoolExecutor compensateBatchPool = ThreadPoolUtil.getCompensateBatchPool();
	private static ThreadPoolExecutor SimulateReqPool = ThreadPoolUtil.getSimulateReqPool();

	public static void main(String[] args) {

		try {
			for (int i = 0; i < 10000; i++) {
				ThreadLocalMemoryEntity threadLocalMemoryEntity = new ThreadLocalMemoryEntity();
				threadLocalMemoryEntity.setName("test" + i);
				//模拟发送请求 实际生产中每一个请求都会有一个线程
				SimulateReqPool.execute(() -> {
					try {
						threadLocal.set(threadLocalMemoryEntity);
						//模拟执行业务逻辑
						compensateBatchPool.execute(() -> {
							//模拟发送kafka消息
							try {
								Thread.sleep(100);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							System.out.println("发送kafka数据:" + threadLocalMemoryEntity.getName());
						});
					} finally {
						//防止内存泄露
						threadLocal.remove();
					}
				});
			}
		} finally {
			compensateBatchPool.shutdown();
			SimulateReqPool.shutdown();

		}

	}
}

注意需要在threadLocal.set 所在的线程 进行 remove才有效,因此在使用ThreadLocal的时候可以遵守这个编程规范

try {
			threadLocal.set(xxx);
		}finally {
			threadLocal.remove();
		}

总结

在多线程编程中,ThreadLocal和ThreadPoolExecutor是两个常用的工具类,但是它们也会带来内存泄露的风险。为了避免这种情况的发生,我们可以在使用完毕后手动删除变量或关闭线程池。希望本文能够对您有所帮助。

你可能感兴趣的:(性能优化,并发编程专题,java)