服务端应用程序(如数据库和 Web 服务器)需要处理来自客户端的高并发、耗时较短的请求任务,所以频繁的创建处理这些请求的所需要的线程就是一个非常消耗资源的操作。常规的方法是针对一个新的请求创建一个新线程,虽然这种方法似乎易于实现,但它有重大缺点。为每个请求创建新线程将花费更多的时间,在创建和销毁线程时花费更多的系统资源。因此同时创建太多线程的 JVM 可能会导致系统内存不足,这就需要限制要创建的线程数,也就是需要使用到线程池。
线程池技术就是线程的重用技术,使用之前创建好的线程来执行当前任务,并提供了针对线程周期开销和资源冲突问题的解决方案。 由于请求到达时线程已经存在,因此消除了线程创建过程导致的延迟,使应用程序得到更快的响应。
上图表示线程池初始化具有3 个线程,任务队列中有5 个待运行的任务对象。
方法 | 描述 |
---|---|
newFixedThreadPool(int) | 创建具有固定的线程数的线程池,int参数表示线程池内线程的数量 |
newCachedThreadPool() | 创建一个可缓存线程池,该线程池可灵活回收空闲线程。若无空闲线程,则新建线程处理任务。 |
newSingleThreadExecutor() | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务 |
newScheduledThreadPool | 创建一个定长线程池,支持定时及周期性任务执行 |
在固定线程池的情况下,如果执行器当前运行的所有线程,则挂起的任务将放在队列中,并在线程变为空闲时执行。
在下面的内容中,我们将介绍线程池的executor执行器。
//第一步: 创建一个任务对象(实现Runnable接口),用于执行具体的任务逻辑 (Step 1) class Task implements Runnable { private String name; public Task(String s) { name = s; } // 打印任务名称并Sleep 1秒 // 整个处理流程执行5次 public void run() { try{ for (int i = 0; i<=5; i++) { if (i==0) { Date d = new Date(); SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss"); System.out.println("任务初始化" + name +" = " + ft.format(d)); //第一次执行的时候,打印每一个任务的名称及初始化的时间 } else{ Date d = new Date(); SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss"); System.out.println("任务正在执行" + name +" = " + ft.format(d)); // 打印每一个任务处理的执行时间 } Thread.sleep(1000); } System.out.println("任务执行完成" + name); } catch(InterruptedException e) { e.printStackTrace(); } } }
测试用例
public class ThreadPoolTest { // 线程池里面最大线程数量 static final int MAX_SIZE = 3; public static void main (String[] args) { // 创建5个任务 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"); // 第二步:创建一个固定线程数量的线程池,线程数为MAX_SIZE ExecutorService pool = Executors.newFixedThreadPool(MAX_SIZE); // 第三步:将待执行的任务对象交给ExecutorService进行任务处理 pool.execute(r1); pool.execute(r2); pool.execute(r3); pool.execute(r4); pool.execute(r5); // 第四步:关闭线程池 pool.shutdown(); } }
任务初始化task 1 = 05:25:55 任务初始化task 2 = 05:25:55 任务初始化task 3 = 05:25:55 任务正在执行task 3 = 05:25:56 任务正在执行task 1 = 05:25:56 任务正在执行task 2 = 05:25:56 任务正在执行task 1 = 05:25:57 任务正在执行task 3 = 05:25:57 任务正在执行task 2 = 05:25:57 任务正在执行task 3 = 05:25:58 任务正在执行task 1 = 05:25:58 任务正在执行task 2 = 05:25:58 任务正在执行task 2 = 05:25:59 任务正在执行task 3 = 05:25:59 任务正在执行task 1 = 05:25:59 任务正在执行task 1 = 05:26:00 任务正在执行task 2 = 05:26:00 任务正在执行task 3 = 05:26:00 任务执行完成task 3 任务执行完成task 2 任务执行完成task 1 任务初始化task 5 = 05:26:01 任务初始化task 4 = 05:26:01 任务正在执行task 4 = 05:26:02 任务正在执行task 5 = 05:26:02 任务正在执行task 4 = 05:26:03 任务正在执行task 5 = 05:26:03 任务正在执行task 5 = 05:26:04 任务正在执行task 4 = 05:26:04 任务正在执行task 4 = 05:26:05 任务正在执行task 5 = 05:26:05 任务正在执行task 4 = 05:26:06 任务正在执行task 5 = 05:26:06 任务执行完成task 4 任务执行完成task 5
如程序执行结果中显示的一样,任务 4 或任务 5 仅在池中的线程变为空闲时才执行。在此之前,额外的任务将放在待执行的队列中。
线程池执行前三个任务,线程池内线程回收空出来之后再去处理执行任务 4 和 5
使用这种线程池方法的一个主要优点是,假如您希望一次处理10000个请求,但不希望创建10000个线程,从而避免造成系统资源的过量使用导致的宕机。您可以使用此方法创建一个包含500个线程的线程池,并且可以向该线程池提交500个请求。
ThreadPool此时将创建最多500个线程,一次处理500个请求。在任何一个线程的进程完成之后,ThreadPool将在内部将第501个请求分配给该线程,并将继续对所有剩余的请求执行相同的操作。在系统资源比较紧张的情况下,线程池是保证程序稳定运行的一个有效的解决方案。
线程池大小优化: 线程池的最佳大小取决于可用的处理器数量和待处理任务的性质。对于CPU密集型任务,假设系统有N个逻辑处理核心,N 或 N+1 的最大线程池数量大小将实现最大效率。对于 I/O密集型任务,需要考虑请求的等待时间(W)和服务处理时间(S)的比例,线程池最大大小为 N*(1+ W/S)会实现最高效率。
不要教条的使用上面的总结,需要根据自己的应用任务处理类型进行灵活的设置与调优,其中少不了测试实验。
原文链接:字母哥博客。
以上就是Java 线程池的作用以及该如何使用的详细内容,更多关于Java 线程池的作用和使用的资料请关注其它相关文章!