线程池使用不当的危害(一):局部变量线程池、类变量线程池的使用方法

前言:

  不知道大家在使用线程池的时候,是否存在以下的疑惑,这是我看到的某些同事的代码,他们是这样使用线程池的:

1、 线程池局部变量,new出来使用,没有手动shutdown
2、 线程池局部变量,new出来使用,并且最后手动shutdown
3、 线程池定义为static类型,进行类复用

大家先想想到底哪种方式是正确的,以及错误的方式可能会带来什么问题,让我们带着疑问去看这篇文章。

  本文探索了局部变量线程池、类变量线程池使用上的坑,和可能导致的问题。最后根据经验和实际上线的项目,给出当前我的线程池使用经验,以及某些创建线程池的变化,供大家学习。

方式一:线程池局部使用,没有shutdown

  首先,我们明确:局部变量new出来的线程池,执行这段代码的程序的每一个线程都会去创建一个局部的线程池。暂且不说每一个线程都去创建线程池是出于什么神奇的目的,首当其冲的线程池的复用的性质就被打破了。创建出来的线程池都得不到复用,那么还有什么必要花费大精力创建线程池?

所以线程池局部使用本身就是不推荐的使用方式!

  其次,我们再来想想,局部使用线程池,同时设置核心线程不为0,且设置allowCoreThreadTimeOut=false(空闲后不回收核心线程池)会导致什么问题?(想都不用想,核心线程池得不到回收,自然会导致OOM)

以下是问题代码:

public static void main(String[] args) {
    
    
        while (true) {
    
    
            try {
    
    

             //newFixedThreadPool不会回收核心线程 可能导致OOM
                ExecutorService service = Executors.newFixedThreadPool(1);
                service.submit(new Runnable() {
    
    
                    @Override
                    public void run() {
    
    
                        try {
    
    
                            Thread.sleep(2000); 模拟处理业务
                        } catch (InterruptedException e) {
    
    
                        }
                    }
                });
                service = null;
            } catch (Exception e) {
    
    
            }
            try {
    
    
                Thread.sleep(2000);
                System.gc();
            } catch (InterruptedException e) {
    
    
            }
        }
    }

  那么,是否我们设置核心线程可以回收不就好咯?同样会出现问题。系统可能会根据你设置的线程过期时间,呈现有规律的内存占用上升,然后下降,然后又上升,然后又下降的趋势。你说说这是好的内存运行情况?

方式二:线程池局部使用,使用完后手动shutdown线程池

  okok,这种方式OOM的风险降低了,但是又是局部使用局部使用,你干嘛要局部使用线程池呢?这样不就使得每一个线程都会new一个线程池,导致线程池不会复用,这和你不用线程池有什么区别呢?系统还白白花费资源去创建线程池。

方式三:线程池定义为static类型,进行类复用

  明显,到这里才是正确的使用线程池的方式。static修饰的类变量只会加载一次,所有的线程共享这一个线程池了呗。以下是正确的使用代码:

/**
 * @author: 代码丰
 * @Description:
 */
public class staticTestExample {
    
    
    //static 定义线程池
    private static ThreadPoolExecutor pool = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            timeout,
            TimeUnit.SECONDS, 
            new LinkedBlockingDeque<>(), 
            new ThreadPoolExecutor.AbortPolicy());
   
    public static void main(String[] args) {
    
    
        //使用线程池
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

业务中的线程池的复用

  这里给出以下两种思路

第一种:根据业务执行类型去创建线程池(同一类型的业务复用一个线程池)
  简单来说,业务场景相同,且需要用到线程池的地方,复用一个线程池。比如,拆分任务场景,一次性需要同时拆分100个任务去执行,就可以把这100个相同业务场景的任务交给一个特定命名的线程池处理。这个线程池就是专门去处理任务拆分的。

代码如下:

/**
 * @author 代码丰
 * 创建线程池工具类
 */
public class ThreadPoolUtil {
    
    
    private static final Map<String, ExecutorService> POOL_CATCH = Maps.newConcurrentMap();

    /**
     * 根据特定名称去缓存线程池
     * @param poolName
     * @return
     */
    public static ExecutorService create(String poolName) {
    
    
        //map有缓存直接复用缓存中存在的线程池
        if (POOL_CATCH.containsKey(poolName)) {
    
    
            return POOL_CATCH.get(poolName);
        }
        // synchronized锁字符串常量池字段 防止并发,使得map中缓存的线程池,只会创建一次
        synchronized (poolName.intern()) {
    
    
            int poolSize = Runtime.getRuntime().availableProcessors() * 2;
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize, poolSize,
                    30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),new ThreadPoolExecutor.AbortPolicy());
            POOL_CATCH.put(poolName, threadPoolExecutor);
            return threadPoolExecutor;
        }
    }
}

	/**
 * @author: 代码丰
 */
public class CorrectTest {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService pool = ThreadPoolUtil.create("拆分任务线程池");
        //线程池执行任务
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

第二种:根据用户登陆性质去创建线程池(用一类型的用户复用一个线程池)
  简单来说,用户类型且业务场景相同,需要用到线程池的地方,复用一个线程池。

/**
 * @author: 代码丰
 * @Description:
 */
public class CorrectTest {
    
    
    public static void main(String[] args) {
    
    
        //模拟得到用户信息
        Userinfo  = getUserinfo();
        //模拟用相同的用户类型(type)去创建线程池
        ExecutorService pool = ThreadPoolUtil.create(Userinfo.getType);
        //线程池执行任务
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

总结

  1. 使用全局线程池而不是局部线程池,否则可能会有连续创建局部线程池的OOM风险
  2. 就算使用局部线程池,最后一定要shutdown,否则可能导致不回收核心线程的内存泄漏
  3. 理解线程池是为了复用的,不要代码中随意new一个局部线程池

猜你喜欢

转载自blog.csdn.net/qq_44716086/article/details/128922458