# 《并发设计模式》第37章-线程池模式-优化社区电商系统优惠券服务

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:初步了解线程池的应用场景,重点理解线程池模式的核心原理和应用,并能够结合自身项目实际场景思考如何将线程池模式灵活应用到自身实际项目中。

大家好,我是冰河~~

充分了解每种线程池的原理、应用场景和注意事项后,在实际项目中使用线程池就会更加得心应手,也会避免很多不必要的坑。在《阿里巴巴Java开发手册》中,就明确规定,禁止在生产环境使用Executors类创建线程池,而应该是使用ThreadPoolExecutor类创建线程池。

# 一、故事背景

虽然小菜按照自己的想法使用线程池处理了问题,但还是会出现CPU占用高的问题,在老王的指导下分析问题,才发现使用Executors类的newCachedThreadPool()方法创建的线程池无法限制创建的线程数量。如果并发量上来,大量要执行的任务被提交到线程池。对于线程池来说,只要有一个任务到来,就必须要有一个线程来处理,如果当前没有空闲的线程处理任务,则会创建一个新的线程来处理任务。这就意味着,使用Executors类的newCachedThreadPool()方法时,如果并发量比较大,就会同时创建并开启大量的线程执行任务,此时就可能导致CPU占用率飙升,并且还可能抛出java.lang.OutOfMemoryError: unable to create new native thread异常。

那这问题到底该如何解决呢?

# 二、不建议使用Executors类

在讲解具体该如何解决问题之前,这里,再从实际业务场景角度跟大家聊聊,为啥不建议在项目中使用Executors类创建线程池。

(1)在实际工作过程中,我们需要根据具体的业务场景,业务体量和预估的并发数,来评估线程池的核心参数,这些核心参数包括:核心线程数、最大线程数、线程回收策略、任务队列和拒绝策略等。确保线程池的运行符合预期,并且在生产环境中,一般会采用可控的有界队列作为线程池的任务队列。

(2)为了便于排查问题,一般会为线程池指定一个有意义的名称。这样,当生产环境出现线程数量激增、死锁、CPU飙升、线程异常等问题时,就可以根据我们指定的名称来抓取线程栈,更快的定位问题和解决问题。

(3)在实际工作中,对于线程池,我们还要监控其任务队列,这是因为在线程池执行任务时,除非触发了拒绝策略,否则,线程池一般不会抛出异常。所以,我们可以通过监控线程池中的任务队列中任务的积压情况,以及线程数量的变化来提早发现问题,并解决问题。

# 三、预估线程数

到底需要为线程池分配多少线程数,其实是需要根据多线程具体的应用场景来确定的。一般情况下,可以将程序分为CPU密集型程序和IO密集型程序,而对于这两种密集型程序来说,计算最佳线程数的方法是不同的。

(1)CPU密集型程序

对于CPU密集型程序来说,多线程重在尽可能多的利用CPU的资源来处理任务。 所以,对于CPU密集型程序来说,理论上“线程的数量=CPU核数”是最合适的。但是在实际工作中,一般会将线程数量设置为“CPU核数+1”,这是为了防止因出现意外情况导致线程阻塞。如果某个线程因意外情况阻塞,则多出来的线程会继续执行任务,从而保证CPU的利用效率。

因此,在CPU密集型的程序中,一般可以将线程数设置为CPU核数+1。

(2)IO密集型程序

对于IO密集型程序来说,如果某个线程在执行IO操作时,另外的线程恰好执行完CPU计算任务,此时CPU能够达到最佳的利用效率。所以,在IO密集型程序中,理论上最佳的线程数与程序中IO操作的耗时和CPU计算的耗时的比值相关。

  • 在单核CPU下,理论上最佳的线程数 = 1 + (IO操作的耗时 / CPU计算的耗时)
  • 在多核CPU下,理论上最佳的线程数 = CPU核数 * [ 1 + (IO操作的耗时 / CPU计算的耗时) ]。

注意:通过上述方式计算出的线程数只是理论上的最佳数量,在实际工作中,还是需要通过对系统不断的进行压测,根据压测的结果数据来找到最佳的线程数。

# 四、解决问题

那到底该如何解决问题呢?其实答案已经很明显了,既要线程数量可控,又要任务队列可控,那最好的方式就是使用自定义线程池。使用自定义线程池解决推送消息问题的流程如图37-1所示。

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码