服务器程序,如数据库和web服务器,反复执行来自多个客户端的请求,这些都是面向处理大量短任务的。构建服务器应用程序的一种方法是在每次请求到达时创建一个新线程,并在新创建的线程中服务这个新请求。虽然这种方法看起来很容易实现,但它有明显的缺点。与处理实际请求相比,为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源更多。
由于活动线程消耗系统资源,因此JVM同时创建太多线程可能会导致系统耗尽内存。这就需要限制正在创建的线程数量。
线程池重用以前创建的线程来执行当前任务,并为线程周期开销和资源抖动问题提供了解决方案。由于请求到达时线程已经存在,因此消除了线程创建带来的延迟,使应用程序响应更快。
- Java提供了Executor框架,该框架以Executor接口、其子接口ExecutorService和类ThreadPoolExecutor为中心,该类实现了这两个接口。通过使用执行器,只需要实现Runnable对象并将它们发送给executor来执行。
- 它们允许您利用线程,但将重点放在希望线程执行的任务上,而不是线程机制上。
- 要使用线程池,我们首先创建一个ExecutorService对象,并将一组任务传递给它。ThreadPoolExecutor类允许设置核心和最大池大小。由特定线程运行的可运行程序按顺序执行。
Executor Thread Pool 方法
方法 | 描述 |
---|---|
newFixedThreadPool(int) | 创建一个固定大小的线程池。 |
newCachedThreadPool() | 创建一个线程池,该线程池根据需要创建新线程,但在以前构造的线程可用时重用它们 |
newSingleThreadExecutor() | 创建一个线程。 |
在固定线程池的情况下,如果executor当前正在运行所有线程,则将挂起的任务放在队列中,并在线程空闲时执行。
Thread Pool 示例
我们将看一个线程池执行器的基本示例——FixedThreadPool。
步骤如下:
1. 创建一个任务(可运行对象)来执行
2. 使用Executor创建Executor池
3.将任务传递给执行器池
4. 关闭执行器池
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 要执行的任务类(步骤1)
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 5 times
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("Initialization Time for"
+ " task name - "+ name +" = " +ft.format(d));
//打印每个任务的初始化时间
}
else
{
Date d = new Date();
SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
System.out.println("Executing Time for task name - "+
name +" = " +ft.format(d));
// 打印每个任务的执行时间
}
Thread.sleep(1000);
}
System.out.println(name+" complete");
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
public class Test
{
// 线程池中的最大线程数
static final int MAX_T = 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_T为no的线程池。
// 线程数作为固定池大小(步骤2)
ExecutorService pool = Executors.newFixedThreadPool(MAX_T);
// 将Task对象传递到池中执行(步骤3)
pool.execute(r1);
pool.execute(r2);
pool.execute(r3);
pool.execute(r4);
pool.execute(r5);
// 池关闭(步骤4)
pool.shutdown();
}
}
示例执行:
Initialization Time for task name - task 3 = 09:20:12
Initialization Time for task name - task 2 = 09:20:12
Initialization Time for task name - task 1 = 09:20:12
Executing Time for task name - task 2 = 09:20:13
Executing Time for task name - task 1 = 09:20:13
Executing Time for task name - task 3 = 09:20:13
Executing Time for task name - task 1 = 09:20:14
Executing Time for task name - task 2 = 09:20:14
Executing Time for task name - task 3 = 09:20:14
Executing Time for task name - task 1 = 09:20:15
Executing Time for task name - task 2 = 09:20:15
Executing Time for task name - task 3 = 09:20:15
Executing Time for task name - task 1 = 09:20:16
Executing Time for task name - task 2 = 09:20:16
Executing Time for task name - task 3 = 09:20:16
task 1 complete
Initialization Time for task name - task 4 = 09:20:17
task 2 complete
Initialization Time for task name - task 5 = 09:20:17
task 3 complete
Executing Time for task name - task 4 = 09:20:18
Executing Time for task name - task 5 = 09:20:18
Executing Time for task name - task 4 = 09:20:19
Executing Time for task name - task 5 = 09:20:19
Executing Time for task name - task 4 = 09:20:20
Executing Time for task name - task 5 = 09:20:20
Executing Time for task name - task 4 = 09:20:21
Executing Time for task name - task 5 = 09:20:21
task 4 complete
task 5 complete
从程序的执行中可以看出,只有当池中的线程变为空闲时,才执行任务4或任务5。在此之前,额外的任务被放置在队列中。
使用这种方法的一个主要优点是,当您希望一次处理100个请求,但不希望为同一个请求创建100个线程时,可以减少JVM过载。您可以使用这种方法创建一个包含10个线程的ThreadPool,并且您可以向该ThreadPool提交100个请求。
ThreadPool将创建最多10个线程来一次处理10个请求。在任何单个线程的进程完成后,
ThreadPool将在内部分配第11个请求给这个线程
并将继续对所有剩余的请求做同样的处理。
使用线程池的风险
1.死锁:虽然死锁可能发生在任何多线程程序中,但线程池引入了另一种死锁情况,在这种情况下,所有正在执行的线程都在等待队列中等待的阻塞线程的结果,因为执行线程不可用。
2.线程泄漏:如果一个线程从池中移除执行任务,但在任务完成后没有返回给它,则会发生线程泄漏。例如,如果线程抛出一个异常,而线程池类没有捕捉到这个异常,那么线程将退出,从而将线程池的大小减少一个。如果重复多次,那么池最终将变为空,并且没有线程可用于执行其他请求。
3.资源震荡:如果线程池的大小非常大,那么在线程之间的上下文切换就会浪费时间。线程数超过最佳数量可能会导致饥饿问题,导致资源抖动,如前所述。
重要的几点
1. 不要让并发等待其他任务结果的任务排队。这可能导致如上所述的死锁情况。
2. 在为长期操作使用线程时要小心。它可能导致线程永远等待,并最终导致资源泄漏。
3. 线程池必须在结束时显式结束。如果没有这样做,那么程序将继续执行并且永远不会结束。在池上调用shutdown()来结束executor。如果您试图在关机后向执行器发送另一个任务,它将抛出一个RejectedExecutionException。
4. 需要了解任务才能有效地调优线程池。如果任务之间的差异很大,那么为不同类型的任务使用不同的线程池是有意义的,以便对它们进行适当的调优。
5. 您可以限制可以在JVM中运行的最大线程数,从而减少JVM耗尽内存的可能性。
6. 如果你需要实现你的循环来创建新的线程进行处理,使用ThreadPool将有助于处理更快,因为ThreadPool在达到它的最大限制后不会创建新的线程。
7. 线程处理完成后,ThreadPool可以使用同一个线程去做另一个进程(这样就节省了创建另一个线程的时间和资源)。
调优线程池
- 线程池的最佳大小取决于可用处理器的数量和任务的性质。在一个有N个处理器的系统上,对于仅由计算类型进程组成的队列,最大线程池大小为N或N+1将获得最大的效率。但是任务可能会等待I/O,在这种情况下,我们会考虑请求的等待时间(W)和服务时间(S)的比率;导致最大池大小为N*(1+ W/S)以获得最大效率。
线程池是组织服务器应用程序的有用工具。它在概念上非常简单,但是在实现和使用它时需要注意几个问题,比如死锁、资源波动。使用执行器服务使其更容易实现。