并发编程与技术内幕:线程池深入理解
Posted 淼淼之森
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程与技术内幕:线程池深入理解相关的知识,希望对你有一定的参考价值。
【本文转载自博主林炳文Evankaka原创文章http://blog.csdn.net/evankaka】
目录:
- 一Executors的API介绍
- 二几种不同的ExecutorService线程池对象
- 三线程池一些常用方法
- 四ThreadPoolExecutor技术内幕
- 五自定义线程池
摘要: 本文主要讲了Java当中的线程池的使用方法、注意事项及其实现源码实现原理,并辅以实例加以说明,对加深Java线程池的理解有很大的帮助。
首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线程实时处理休眠状态,等待唤醒执行。那么为什么要有线程池这个东西呢?可以从以下几个方面来考虑:
其一、减少在创建和销毁线程上所花的时间以及系统资源的开销 ,解决资源不足的问题;
其二、将当前任务与主线程隔离,能实现和主线程的异步执行,特别是很多可以分开重复执行的任务。但是,一味的开线程也不一定能带来性能上的优化,线池休眠也是要占用一定的内存空间,所以合理的选择线程池的大小也是有一定的依据。
为什么需要线程池?
基于以下几个原因在多线程应用程序中使用线程是必须的:
1. 线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。
2. 线程池节省了CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。
3. 线程池根据当前在系统中运行的进程来优化线程时间片。
4. 线程池允许我们开启多个任务而不用为每个线程设置属性。
5. 线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。
6. 线程池可以用来解决处理一个特定请求最大线程数量限制问题。
影响设计一个多线程应用程序的因素有:
1. 一个应用程序的响应时间。
2. 线程管理资源的分配。
3. 资源共享。
4. 线程同步。
一、Executors的API介绍
Java类库提供了许多静态方法来创建一个线程池:
newCachedThreadPool | 创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制 |
newFixedThreadPool | 创建一个固定长度的线程池,当到达线程最大数量时,线程池的规模将不再变化 |
newSingleThreadPoolExecutor | 创建一个单线程的Executor,确保任务对了,串行执行 |
newScheduledThreadPool | 创建一个固定长度的线程池,而且以延迟或者定时的方式来执行,类似Timer |
小结一下:在线程池中执行任务比为每个任务分配一个线程优势更多,通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊线程创建和销毁产生的巨大的开销。当请求到达时,通常工作线程已经存在,提高了响应性;通过配置线程池的大小,可以创建足够多的线程使CPU达到忙碌状态,还可以防止线程太多耗尽计算机的资源。
创建线程池基本方法:
(1)定义线程类
class Handler implements Runnable { …… }
(2)建立ExecutorService线程池
ExecutorService executorService = Executors.newCachedThreadPool();
或者
int cpuNums = Runtime.getRuntime().availableProcessors(); //获取当前系统的CPU 数目 ExecutorService executorService =Executors.newFixedThreadPool(cpuNums * POOL_SIZE); //ExecutorService通常根据系统资源情况灵活定义线程池大小
CPU手动查看方式:任务管理器-->性能-->CPU使用记录(此处有几个小窗口就表示CPU核心数目的个数)
(3)调用线程池操作
循环操作,成为daemon,把新实例放入Executor池中
while(true){ executorService.execute(new Handler(socket)); // class Handler implements Runnable{ 或者 executorService.execute(createTask(i)); //private static Runnable createTask(final int taskID) }
二、几种不同的ExecutorService线程池对象
1.CachedThreadPool() | -缓存型池子,先查看池中有没有以前建立的线程,如果有,就reuse.如果没有,就建一个新的线程加入池中 -缓存型池子通常用于执行一些生存期很短的异步型任务 因此在一些面向连接的daemon型SERVER中用得不多。 -能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。 |
2.FixedThreadPool |
-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程 |
3.ScheduledThreadPool | -调度型线程池 -这个池子里的线程可以按schedule依次delay执行,或周期执行 |
4.SingleThreadExecutor | -单例线程,任意时间池中只能有一个线程 -用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE) |
Java学习交流QQ群:603654340 我们一起学Java!
应用实例:
1.newCachedThreadPool
CachedThreadPool首先会按照需要创建足够多的线程来执行任务(Task)。随着程序执行的过程,有的线程执行完了任务,可以被重新循环使用时,才不再创建新的线程来执行任务。我们采用《Thinking In Java》中的例子来分析。客户端线程和线程池之间会有一个任务队列。当程序要关闭时,你需要注意两件事情:入队的这些任务的情况怎么样了以及正在运行的这个任务执行得如 何了。令人惊讶的是很多开发人员并没能正确地或者有意识地去关闭线程池。正确的方法有两种:一个是让所有的入队任务都执行完毕(shutdown()), 再就是舍弃这些任务(shutdownNow())——这完全取决于你。比如说如果我们提交了N多任务并且希望等它们都执行完后才返回的话,那么就使用 shutdown()。
例子:
1 package com.mmz.OtherTest; 2 3 import java.util.Date; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 7 /** 8 * 功能概要:缓冲线程池实例-execute运行 9 */ 10 class Handle implements Runnable { 11 private String name; 12 public Handle(String name) { 13 this.name = name; 14 } 15 @Override 16 public void run() { 17 System.out.println("线程" + name +" Start.Time = "+new Date()); 18 processCommand(); 19 System.out.println("线程" + name +" End.Time = "+new Date()); 20 } 21 private void processCommand() { 22 try { 23 Thread.sleep(1000); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 @Override 29 public String toString(){ 30 return this.name; 31 } 32 } 33 34 //验证CachedThreadPool 35 public class testCachedThreadPool{ 36 public static void main(String[] args) { 37 System.out.println("Main: Starting at: "+ new Date()); 38 ExecutorService exec = Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE 39 for(int i = 0; i < 10; i++) { 40 exec.execute(new Handle(String.valueOf(i))); 41 } 42 exec.shutdown(); //执行到此处并不会马上关闭线程池,但之后不能再往线程池中加线程,否则会报错 43 System.out.println("Main: Finished all threads at"+ new Date()); 44 } 45 }
1 Main: Starting at: Wed Oct 25 18:08:11 CST 2017 2 线程1 Start.Time = Wed Oct 25 18:08:11 CST 2017 3 线程2 Start.Time = Wed Oct 25 18:08:11 CST 2017 4 线程0 Start.Time = Wed Oct 25 18:08:11 CST 2017 5 线程3 Start.Time = Wed Oct 25 18:08:11 CST 2017 6 线程4 Start.Time = Wed Oct 25 18:08:11 CST 2017 7 线程5 Start.Time = Wed Oct 25 18:08:11 CST 2017 8 线程6 Start.Time = Wed Oct 25 18:08:11 CST 2017 9 线程8 Start.Time = Wed Oct 25 18:08:11 CST 2017 10 线程7 Start.Time = Wed Oct 25 18:08:11 CST 2017 11 线程9 Start.Time = Wed Oct 25 18:08:11 CST 2017 12 Main: Finished all threads atWed Oct 25 18:08:11 CST 2017 13 线程0 End.Time = Wed Oct 25 18:08:12 CST 2017 14 线程4 End.Time = Wed Oct 25 18:08:12 CST 2017 15 线程3 End.Time = Wed Oct 25 18:08:12 CST 2017 16 线程8 End.Time = Wed Oct 25 18:08:12 CST 2017 17 线程7 End.Time = Wed Oct 25 18:08:12 CST 2017 18 线程2 End.Time = Wed Oct 25 18:08:12 CST 2017 19 线程1 End.Time = Wed Oct 25 18:08:12 CST 2017 20 线程5 End.Time = Wed Oct 25 18:08:12 CST 2017 21 线程6 End.Time = Wed Oct 25 18:08:12 CST 2017 22 线程9 End.Time = Wed Oct 25 18:08:12 CST 2017
从上面的结果可以看出:
1、主线程的执行与线程池里的线程分开,有可能主线程结束了,但是线程池还在运行
2、放入线程池的线程并不一定会按其放入的先后而顺序执行
2.newFixedThreadPool
FixedThreadPool模式会使用一个优先固定数目的线程来处理若干数目的任务。规定数目的线程处理所有任务,一旦有线程处理完了任务就会被用来处理新的任务(如果有的话)。这种模式与上面的CachedThreadPool是不同的,CachedThreadPool模式下处理一定数量的任务的线程数目是不确定的。而FixedThreadPool模式下最多 的线程数目是一定的。
例子:
1 package com.mmz.OtherTest; 2 3 import java.util.Date; 4 import java.util.Random; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.TimeUnit; 8 9 public class testFixedThreadPool { 10 public static void main(String[] args) { 11 System.out.println("Main Thread: Starting at: "+ new Date()); 12 ExecutorService exec = Executors.newFixedThreadPool(5); 13 for(int i = 0; i < 10; i++) { 14 exec.execute(new Handle(String.valueOf(i))); 15 } 16 exec.shutdown(); //执行到此处并不会马上关闭线程池 17 System.out.println("Main Thread: Finished at:"+ new Date()); 18 } 19 }
1 Main Thread: Starting at: Wed Oct 25 18:12:12 CST 2017 2 线程0 Start.Time = Wed Oct 25 18:12:12 CST 2017 3 线程1 Start.Time = Wed Oct 25 18:12:12 CST 2017 4 线程3 Start.Time = Wed Oct 25 18:12:12 CST 2017 5 线程2 Start.Time = Wed Oct 25 18:12:12 CST 2017 6 线程4 Start.Time = Wed Oct 25 18:12:12 CST 2017 7 Main Thread: Finished at:Wed Oct 25 18:12:12 CST 2017 8 线程3 End.Time = Wed Oct 25 18:12:13 CST 2017 9 线程5 Start.Time = Wed Oct 25 18:12:13 CST 2017 10 线程0 End.Time = Wed Oct 25 18:12:13 CST 2017 11 线程6 Start.Time = Wed Oct 25 18:12:13 CST 2017 12 线程1 End.Time = Wed Oct 25 18:12:13 CST 2017 13 线程7 Start.Time = Wed Oct 25 18:12:13 CST 2017 14 线程4 End.Time = Wed Oct 25 18:12:13 CST 2017 15 线程8 Start.Time = Wed Oct 25 18:12:13 CST 2017 16 线程2 End.Time = Wed Oct 25 18:12:13 CST 2017 17 线程9 Start.Time = Wed Oct 25 18:12:13 CST 2017 18 线程5 End.Time = Wed Oct 25 18:12:14 CST 2017 19 线程9 End.Time = Wed Oct 25 18:12:14 CST 2017 20 线程8 End.Time = Wed Oct 25 18:12:14 CST 2017 21 线程7 End.Time = Wed Oct 25 18:12:14 CST 2017 22 线程6 End.Time = Wed Oct 25 18:12:14 CST 2017
从上面的结果可以看出:
1、上面创建了一个固定大小的线程池,大小为5,也就说同一时刻最多只有5个线程能运行。并且线程执行完成后就从线程池中移出。
2、它也不能保证放入的线程能按顺序执行。这要看在等待运行的线程的竞争状态了。
3、newSingleThreadExecutor
其实这个就是创建只能运行一条线程的线程池。它能保证线程的先后顺序执行,并且能保证一条线程执行完成后才开启另一条新的线程
例子:
1 package com.mmz.OtherTest; 2 3 import java.util.Date; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 7 public class testnewSingleThreadExecutor { 8 public static void main(String[] args) { 9 System.out.println("Main Thread: Starting at: "+ new Date()); 10 ExecutorService exec = Executors.newSingleThreadExecutor(); //创建大小为1的固定线程池 11 for(int i = 0; i < 10; i++) { 12 exec.execute(new Handle(String.valueOf(i))); 13 } 14 exec.shutdown(); //执行到此处并不会马上关闭线程池 15 System.out.println("Main Thread: Finished at:"+ new Date()); 16 } 17 } 18 19
1 Main Thread: Starting at: Wed Oct 25 18:28:34 CST 2017 2 线程0 Start.Time = Wed Oct 25 18:28:34 CST 2017 3 Main Thread: Finished at:Wed Oct 25 18:28:34 CST 2017 4 线程0 End.Time = Wed Oct 25 18:28:35 CST 2017 5 线程1 Start.Time = Wed Oct 25 18:28:35 CST 2017 6 线程1 End.Time = Wed Oct 25 18:28:36 CST 2017 7 线程2 Start.Time = Wed Oct 25 18:28:36 CST 2017 8 线程2 End.Time = Wed Oct 25 18:28:37 CST 2017 9 线程3 Start.Time = Wed Oct 25 18:28:37 CST 2017 10 线程3 End.Time = Wed Oct 25 18:28:38 CST 2017 11 线程4 Start.Time = Wed Oct 25 18:28:38 CST 2017 12 线程4 End.Time = Wed Oct 25 18:28:39 CST 2017 13 线程5 Start.Time = Wed Oct 25 18:28:39 CST 2017 14 线程5 End.Time = Wed Oct 25 18:28:40 CST 2017 15 线程6 Start.Time = Wed Oct 25 18:28:40 CST 2017 16 线程6 End.Time = Wed Oct 25 18:28:41 CST 2017 17 线程7 Start.Time = Wed Oct 25 18:28:41 CST 2017 18 线程7 End.Time = Wed Oct 25 18:28:42 CST 2017 19 线程8 Start.Time = Wed Oct 25 18:28:42 CST 2017 20 线程8 End.Time = Wed Oct 25 18:28:43 CST 2017 21 线程9 Start.Time = Wed Oct 25 18:28:43 CST 2017 22 线程9 End.Time = Wed Oct 25 18:28:44 CST 2017
其实它也等价于以下:
ExecutorService exec = Executors.newFixedThreadPool(1);
从上面的结果可以看出:
线程池中始终只有一条线程在运行;并且保证一条线程执行完成后才开启另一条新的线程。
4、newScheduledThreadPool
这是一个计划线程池类,它能设置线程执行的先后间隔及执行时间等,功能比上面的三个强大了一些。
例子:
1 package com.mmz.OtherTest; 2 3 import java.util.Date; 4 import java.util.Random; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.ScheduledThreadPoolExecutor; 8 import java.util.concurrent.TimeUnit; 9 10 public class testnewScheduledThreadPool { 11 public static void main(String[] args) { 12 System.out.println("Main Thread: Starting at: "+ new Date()); 13 ScheduledThreadPoolExecutor exec = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(10); //创建大小为10的线程池 14 for(int i = 0; i < 10; i++) { 15 exec.schedule(new Handle(String.valueOf(i)), 10, TimeUnit.SECONDS);//延迟10秒执行 16 } 17 exec.shutdown(); //执行到此处并不会马上关闭线程池 18 while(!exec.isTerminated()){ 19 //wait for all tasks to finish 20 } 21 System.out.println("Main Thread: Finished at:"+ new Date()); 22 } 23 } 24 25
Main Thread: Starting at: Wed Oct 25 18:25:57 CST 2017 线程1 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程2 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程3 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程0 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程9 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程6 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程8 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程7 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程5 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程4 Start.Time = Wed Oct 25 18:26:07 CST 2017 线程2 End.Time = Wed Oct 25 18:26:08 CST 2017 线程1 End.Time = Wed Oct 25 18:26:08 CST 2017 线程0 End.Time = Wed Oct 25 18:26:08 CST 2017 线程6 End.Time = Wed Oct 25 18:26:08 CST 2017 线程9 End.Time = Wed Oct 25 18:26:08 CST 2017 线程3 End.Time = Wed Oct 25 18:26:08 CST 2017 线程8 End.Time = Wed Oct 25 18:26:08 CST 2017 线程7 End.Time = Wed Oct 25 18:26:08 CST 2017 线程5 End.Time = Wed Oct 25 18:26:08 CST 2017 线程4 End.Time = Wed Oct 25 18:26:08 CST 2017 Main Thread: Finished at:Wed Oct 25 18:26:08 CST 2017
ScheduledThreadPoolExecutor的定时方法主要有以下四种:
下面将主要来具体讲讲scheduleAtFixedRate和scheduleWithFixedDelay
scheduleAtFixedRate 按指定频率周期执行某个任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
command:执行线程
initialDelay:初始化延时
period:两次开始执行最小间隔时间
unit:计时单位
scheduleWithFixedDelay 周期定时执行某个任务/按指定频率间隔执行某个任务(注意)
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
command:执行线程
initialDelay:初始化延时
period:前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
unit:计时单位
使用实例:
class MyHandle implements Runnable { @Override public void run() { System.out.println(System.currentTimeMillis()); try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
1.按指定频率周期执行某个任务
下面实现每隔2秒执行一次,注意,如果上次的线程还没有执行完成,那么会阻塞下一个线程的执行。即使线程池设置得足够大。
/** * 初始化延迟0ms开始执行,每隔2000ms重新执行一次任务 * @author linbingwen * @since 2016年6月6日 */ public static void executeFixedRate() { ScheduledExecutorService executor = Executors.newScheduledThreadPool(10); executor.scheduleAtFixedRate( new MyHandle(), 0, 2000, TimeUnit.MILLISECONDS); }
间隔指的是连续两次任务开始执行的间隔。对于scheduleAtFixedRate方法,当执行任务的时间大于我们指定的间隔时间时,它并不会在指定间隔时开辟一个新的线程并发执行这个任务。而是等待该线程执行完毕。
2、按指定频率间隔执行某个任务
/** * 以固定延迟时间进行执行 * 本次任务执行完成后,需要延迟设定的延迟时间,才会执行新的任务 */ public static void executeFixedDelay() { ScheduledExecutorService executor = Executors.newScheduledThreadPool(10); executor.scheduleWithFixedDelay( new MyHandle(), 0, 2000, TimeUnit.MILLISECONDS); }
间隔指的是连续上次执行完成和下次开始执行之间的间隔。
3.周期定时执行某个任务
周期性的执行一个任务,可以使用下面方法设定每天在固定时间执行一次任务。
/** * 每天晚上9点执行一次 * 每天定时安排任务进行执行 */ public static void executeEightAtNightPerDay() { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); long oneDay = 24 * 60 * 60 * 1000; long initDelay = getTimeMillis("21:00:00") - System.currentTimeMillis(); initDelay = initDelay > 0 ? initDelay : oneDay + initDelay; executor.scheduleAtFixedRate( new MyHandle(), initDelay, oneDay, TimeUnit.MILLISECONDS); } /** * 获取指定时间对应的毫秒数 * @param time "HH:mm:ss" * @return */ private static long getTimeMillis(String time) { try { DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); DateFormat dayFormat = new SimpleDateFormat("yy-MM-dd"); Date curDate = dateFormat.parse(dayFormat.format(new Date()) + " " + time); return curDate.getTime(); } catch (ParseException e) { e.printStackTrace(); } return 0; }
三、线程池一些常用方法
1、submit()
将线程放入线程池中,除了使用execute,也可以使用submit,它们两个的区别是一个使用没有返回值,一个有返回值。
submit的方法很适应于生产者-消费者模式,通过和Future结合一起使用,可以起到如果线程没有返回结果,就阻塞当前线程等待线程 池结果返回。
它主要有三种方法:
一般用第一种比较多
如下实例。注意,submit中的线程要实现接口Callable:
1 package com.mmz.OtherTest; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import java.util.concurrent.Callable; 6 import java.util.concurrent.ExecutionException; 7 import java.util.concurrent.ExecutorService; 8 import java.util.concurrent.Executors; 9 import java.util.concurrent.Future; 10 11 /** 12 * 功能概要:缓冲线程池实例-submit运行 13 */ 14 class TaskWithResult implements Callable<String> { 15 private int id; 16 17 public TaskWithResult(int id) { 18 this.id = id; 19 } 20 21 /** 22 * 任务的具体过程,一旦任务传给ExecutorService的submit方法,则该方法自动在一个线程上执行。 23 * 24 * @return 25 * @throws Exception 26 */ 27 public String call() throws Exception { 28 System.out.println("call()方法被自动调用,干活!!! " + Thread.currentThread().getName()); 29 //一个模拟耗时的操作 30 for (int i = 999999; i > 0; i--) ; 31 return"call()方法被自动调用,任务的结果是:" + id + " " + Thread.currentThread().getName(); 32 } 33 } 34 35 public class test7 { 36 public static void main(String[] args) { 37 ExecutorService executorService = Executors.newCachedThreadPool(); 38 List<Future<String>> resultList = new ArrayList<Future<String>>(); 39 40 //创建10个任务并执行 41 for (int i = 0; i < 10; i++) { 42 //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中 43 Future<String> future = executorService.submit(new TaskWithResult(i)); 44 //将任务执行结果存储到List中 45 resultList.add(future); 46 } 47 //启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。 48 executorService.shutdown(); 49 50 //遍历任务的结果 51 for (Future<String> fs : resultList) { 52 try { 53以上是关于并发编程与技术内幕:线程池深入理解的主要内容,如果未能解决你的问题,请参考以下文章
Java并发编程与技术内幕:ArrayBlockingQueueLinkedBlockingQueue及SynchronousQueue源码解析