线程池学习-【转】一段JAVA线程池的设置引发的血案

程老师原文地址:http://flychao88.iteye.com/blog/2228423
原文如下:

 应公司需求我们对一个项目进行了线上压力测试,结果发现,三台服务器一共只有59TPS,结果惨不忍睹。

那么针对这样的场景,我们利用一周时间进行专注性的优化,寻找性能的瓶颈点。

 

第一步:我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的C3P0。

那么当压测到二万批,100个用户同时访问的时候,并发量突然降为零!报错如下:

Java代码   收藏代码
  1. Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.  

 

针对以上错误跟踪C3P0源码,以及在网上搜索资料(http://blog.sina.com.cn/s/blog_53923f940100g6as.html)发现,C3P0在大并发下表现的性能不佳。

 

第二步:针对这个问题进行数据库连接池优化,更换了BoneCPDataSource,以及Apache BasicDataSource后,发现报错如下:

           

Java代码   收藏代码
  1. java.lang.OutOfMemoryError: unable to create new native thread, dubbo version: 2.5.4, current host: 192.168.122.1  
  2. java.lang.OutOfMemoryError: unable to create new native thread  

 

第三步:由此可以判断,问题不在于连接池的问题,于是在压测的时候,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。

             于是迅速定位到代码,发现如下代码:

         

Java代码   收藏代码
  1. private static final ExecutorService executorService = Executors.newCachedThreadPool();  
  2. /** 
  3. * 异步执行短频快的任务 
  4. * @param task 
  5. */  
  6. public static void asynShortTask(Runnable task){  
  7. executorService.submit(task);  
  8. //task.run();  
  9. }  
  10.   
  11.            CommonUtils.asynShortTask(new Runnable() {  
  12.                 @Override  
  13.                 public void run() {  
  14.                     String sms = sr.getSmsContent();  
  15.                     sms = sms.replaceAll(finalCode, AES.encryptToBase64(finalCode, ConstantUtils.getDB_AES_KEY()));  
  16.                     sr.setSmsContent(sms);  
  17.                     smsManageService.addSmsRecord(sr);  
  18.                 }  
  19.             });  

 

 

那么问题到底在哪里呢???就在这一行!

Java代码   收藏代码
  1. private static final ExecutorService executorService = Executors.newCachedThreadPool();  

 

在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer的最大值!看如下源码:

Java代码   收藏代码
  1. public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {  
  2.     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
  3.                                   60L, TimeUnit.SECONDS,  
  4.                                   new SynchronousQueue<Runnable>(),  
  5.                                   threadFactory);  
  6. }  

 

那么尝试修改成如下代码:

Java代码   收藏代码
  1. private static final ExecutorService executorService = Executors.newFixedThreadPool(50);  

 

 

修改完成以后,并发量重新上升到100以上TPS,但是当并发量非常大的时候,项目GC(垃圾回收能力下降),分析原因还是因为 Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但是

当并发量非常大的时候,采用newFixedThreadPool这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下:

Java代码   收藏代码
  1. public static ExecutorService newFixedThreadPool(int nThreads) {  
  2.     return new ThreadPoolExecutor(nThreads, nThreads,  
  3.                                   0L, TimeUnit.MILLISECONDS,  
  4.                                   new LinkedBlockingQueue<Runnable>());  
  5. }  

 可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。

  

结论:

目前我们的项目还在持续优化中,还没有最终优化完成,目标是要把项目优化完善,但此次事件,再次提醒我们,在使用线程池的时候,一定要把握其细节,深入了解其原理再使用,不要随意使用,任何线程池的使用方式都有不同的使用场景,并不是只要使用了线程池就万事大吉,还有很多工作需要我们去注意。

********************************学习笔记开始********************************

看完了还是很有感触的,因为我就是这么使用线程池的,只不过业务关系没有那么大量,触发不了文章的场景。

背景:

线程池主要是解决复用跟流控(防止系统资源耗尽)

参数:

如果是使用简单实现如下。

ExecutorService executorService = Executors.newFixedThreadPool(50);  

当然还可以根据自己业务需求通过ThreadPoolExecutor来创建一个线程池,先把线程池的参数列一下:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
  1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  4. PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n  AbortPolicy:直接抛出异常。

  1. CallerRunsPolicy:只用调用者所在线程来运行任务。
  2. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  3. DiscardPolicy:不处理,丢弃掉。
  4. 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

demo代码,:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;

public class ThreadPoolTest {

public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler rej = new RejectedExecutionHandlerImpl();
//create ThreadPoolExecutor
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 4, 10, TimeUnit.MILLISECONDS,
queue, threadFactory, rej);
//monitor
MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
monitor.start();
//execute no return
for(int i=0; i<10; i++){
executorPool.execute(new WorkerThread("cmd"+i));
}

Thread.sleep(10000);
//shut down the pool
executorPool.shutdown();
//shut down the monitor thread
Thread.sleep(5000);
monitor.shutdown();
}
}

线程池处理任务的优先级为: 
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。我们初始线程池大小设为2、最大值设为4、工作队列大小设为2。所以,如果当前有4个任务正在运行而此时又有新任务提交,工作队列将只存储2个任务和其他任务将交由RejectedExecutionHandlerImpl (demo中只是打印信息)处理。

运行结果如下:

线程池学习-【转】一段JAVA线程池的设置引发的血案 - bohu83 - bohu83的博客

 ***********************总结*********************

如何配置线程池,需要根据任务角度去考虑,CPU密集型,还是IO密集型,是否根据优先级使用PriorityBlockingQueue等等。还有一点,就是考虑业务的波峰、波谷去调整最新,最大,保存时间等参数。通常还是推荐使用有界阻塞队列。

对于上文程老师的这个场景,异步执行短频快的需求,代码就是下发短信。单个节点已经不能满足。能够采用MQ这种方式进行解耦处理,即把任务推送到mq中。有多个节点并发的去获取后处理。毕竟每个 节点的资源有限。这样根据压测设置合理线程池大小,解决了单个节点资源不足的情况。


参考:

http://www.importnew.com/8542.html

http://ifeve.com/java-threadpool/


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值