服务端的程序,例如数据库服务器和Web服务器,每次收到客户端的请求,都会创建一个线程来处理这些请求。
创建线程的方式又很多,例如继承Thread类、实现Runnable或者Callable接口等。
通过创建新的线程来处理客户端的请求,这种看起来很容易的方法,其实是有很大弊端且有很高的风险的。
俗话说,简单的路越走越困难,困难的路越走越简单,就是这个道理。
创建和销毁线程,会消耗大量的服务器资源,甚至创建和销毁线程消耗的时间比线程本身处理任务的时间还要长。
由于启动线程需要消耗大量的服务器资源,如果创建过多的线程会造成系统内存不足(run out of memory),因此限制线程创建的数量十分必要。
线程池通俗来讲就是一个取出和放回提前创建好的线程的池子,概念上,类似数据库的连接池。
那么线程池是如何发挥作用的呢?
实际上,线程池是通过重用之前创建好线程来处理当前任务,来达到大大降低线程频繁创建和销毁导致的资源消耗的目的。
A thread pool reuses previously created threads to execute current tasks and offers a solution to the problem of thread cycle overhead and resource thrashing. Since the thread is already existing when the request arrives, the delay introduced by thread creation is eliminated, making the application more responsive.
下面总结一下开篇对于线程池的一些介绍。
但到底怎么使用线程池呢?线程池真的这么简单好用吗?线程池使用的过程中有没有什么坑?
不要着急,下面就结合具体的示例,跟你讲解各种使用线程池的姿势,以及这些姿势爽在哪里,痛在哪里。
准备好纸巾,咳咳...,是笔记本,涛哥要跟你开讲啦!
java.util.concurrent.Executors
是JDK的并发包下提供的一个工厂类(Factory)和工具类(Utility)。
Executors
提供了关于 Executor
, ExecutorService
, ScheduledExecutorService
, ThreadFactory
和 Callable
相关的工厂方法和工具方法。
Executor
是一个执行提交的 Runnable Tasks
的对象,它有一个 execute
方法,参数是 Runnable
。当执行 execute
方法以后,会在未来某个时间,通过创建线程或者使用线程池中的线程的方式执行参数中的任务。用法如下:
Executor executor = anExecutor; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2()); 复制代码
ExecutorService
继承了 Executor
,并提供了更多有意思的方法,比如 shutdown
方法会让 ExecutorService
拒绝创建新的线程来执行task。
//创建固定线程数量的线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); //创建一个线程池,该线程池会根据需要创建新的线程,但如果之前创建的线程可以使用,会重用之前创建的线程 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //创建一个只有一个线程的线程池 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); 复制代码
下面我就创建5个Task,并通过一个包含3个线程的线程池来执行任务。我们一起看下会发生什么。
Github 完整代码: 一个线程池的例子
☝
ThreadPoolExample1
就是我们的测试类,下面所有的内部类、常量和方法都写在这个测试类里。
package net.ijiangtao.tech.concurrent.jsd.threadpool; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample1 { } 复制代码
Task
内部类执行了两次for循环,并在每次循环执行结束以后 sleep 1秒钟。
// Task class to be executed (Step 1) static class Task implements Runnable { private String name; public Task(String s) { name = s; } // Prints task name and sleeps for 1s // This Whole process is repeated 2 times public void run() { try { for (int i = 0; i <= 1; i++) { if (i == 0) { //prints the initialization time for every task printTimeMsg("Initialization"); } else { // prints the execution time for every task printTimeMsg("Executing"); } Thread.sleep(1000); } System.out.println(name + " complete"); } catch (InterruptedException e) { e.printStackTrace(); } } private void printTimeMsg(String state) { Date d = new Date(); SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss"); System.out.println(state+" Time for"+ " task name - " + name + " = " + ft.format(d)); } } 复制代码
创建一个固定线程数的线程池。
// Maximum number of threads in thread pool static final int MAX_T = 3; // creates a thread pool with MAX_T no. of // threads as the fixed pool size(Step 2) private static final ExecutorService pool = Executors.newFixedThreadPool(MAX_T); 复制代码
创建5个任务,并通过线程池的线程执行这些任务。
public static void main(String[] args) { // creates five tasks Runnable r1 = new Task("task 1"); Runnable r2 = new Task("task 2"); Runnable r3 = new Task("task 3"); Runnable r4 = new Task("task 4"); Runnable r5 = new Task("task 5"); // passes the Task objects to the pool to execute (Step 3) pool.execute(r1); pool.execute(r2); pool.execute(r3); pool.execute(r4); pool.execute(r5); // pool shutdown ( Step 4) pool.shutdown(); } 复制代码
执行结果如下。
Initialization Time for task name - task 1 = 12:39:44 Initialization Time for task name - task 2 = 12:39:44 Initialization Time for task name - task 3 = 12:39:44 Executing Time for task name - task 3 = 12:39:45 Executing Time for task name - task 1 = 12:39:45 Executing Time for task name - task 2 = 12:39:45 task 2 complete Initialization Time for task name - task 4 = 12:39:46 task 3 complete Initialization Time for task name - task 5 = 12:39:46 task 1 complete Executing Time for task name - task 5 = 12:39:47 Executing Time for task name - task 4 = 12:39:47 task 5 complete task 4 complete 复制代码
从输出的结果我们可以看到,5个任务在包含3个线程的线程池执行。
下面是我第二次执行的结果。
Initialization Time for task name - task 1 = 12:46:33 Initialization Time for task name - task 3 = 12:46:33 Initialization Time for task name - task 2 = 12:46:33 Executing Time for task name - task 2 = 12:46:34 Executing Time for task name - task 3 = 12:46:34 Executing Time for task name - task 1 = 12:46:34 task 3 complete task 2 complete task 1 complete Initialization Time for task name - task 4 = 12:46:35 Initialization Time for task name - task 5 = 12:46:35 Executing Time for task name - task 4 = 12:46:36 Executing Time for task name - task 5 = 12:46:36 task 5 complete task 4 complete 复制代码
task 1 2 3 获得线程资源,task 4 5排队等待:
task 1 2 3 执行结束,task 4 5获得线程资源,线程池中有一个线程处于空闲状态:
但规律是相同的,那就是线程池会将自己的线程资源贡献出来,如果任务数超出了线程池的线程数,就会阻塞并排队等待有可用的线程资源以后执行。
也就是线程池会保证你的task在将来(Future)的某个时间执行,但并不能保证什么时间会执行。
相信你现在对于 ExecutorService
的 invokeAll
方法,可以执行一批task并返回一个 Future
集合,就会有更深入的理解了。
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException 复制代码
通过 ExecutorService
线程池执行task的过程如下图所示,超出线程池线程数量的task将会在 BlockingQueue
排队等待获得线程资源的机会。
关于并发编程中的Futrue,笔者有一篇文章( Java并发编程-Future系列之Future的介绍和基本用法 )专门介绍,请通过下面任意的链接移步欣赏:
本教程带领大家了解了线程池的来由、概念和基本用法,相信大家看完,以后就不再只会傻傻地 new Thread
了。
本节只是线程池的入门,下面会介绍关于线程池的更多 武功秘籍
,希望大家持续关注,有所获益。