1.线程池的概念
在前面的章节中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为系统启动一个新线程的成本是比较高的,它涉及到与操作系统的交互,频繁创建线程和销毁线程都需要时间。
在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,但系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃。而线程池的最大线程数参数可以控制系统中并发的线程不超过此数目。
合理利用线程池能够带来三个好处:
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
在JDK1.5之前,开发者必须手动的实现自己的线程池,从JDK1.5之后,Java内建支持线程池。与多线程并发的所有支持的类都在java.lang.concurrent包中,我们可以使用里面的类更加的控制多线程的执行。
2.Runnable与Callable详解
我们目前已经学习了创建线程的两种方式,一种是直接继承Thread类,另外一种就是实现Runnable接口。
但是这两种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果,并且在run方法中不允许抛出异常。
接下来,我们就深入的学习一下Runnable接口与Callable接口。
- Runnable接口
先说一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法,在run()方法中封装的都是线程任务。
public interface Runnable {
public abstract void run();
}
由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果,并且也不允许抛出任何异常。
Runnable接口启动线程的方法通常为Thread类的start()方法,也可以通过ExecutorService接口的submit()方法来实现。

ExecutorService指的就是一个线程池对象,关于ExecutorService我们后续章节再讲解。
- Callable接口
Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call(),在call()方法中封装的也是线程任务。
public interface Callable<V> {
V call() throws Exception;
}
可以源码中可以看到,Callable是一个泛型接口,call()方法返回的类型就是传递进来的V类型,并可以在call()方法体中抛出异常。
那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,使用ExecutorService接口中的submit()方法来其启动线程池中的某一个线程,然后再执行Callable接口实现类中封装的线程任务。

3.Executors类
JDK1.5中提供Executors工厂类来产生线程池,该工厂类中包含如下的几个静态工程方法来创建线程池,我们只需要调用对应的静态方法就可以获的想要的线程池对象。

通过Executors工厂类可以生产两种类型的线程池对象,ExecutorService代表可尽快执行线程任务的线程池,ScheduleExecutorService代表可延迟执行线程任务的线程池,接下来我们就对这两种线程池来深入的讲解。
4.ExecutorService接口
可以看到上面的五个方法中,前面三个方法的返回值都是一个ExecutorService对象。该ExecutorService对象就代表着一个尽快执行线程任务的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象提交给该线程池即可,该线程就会尽快的执行该任务。
ExecutorService将Runnable对象或Callable对象提交给该线程池执行任务的方法:

接下来,我们就来学习一下线程池的简单使用,如何通过线程池的submit方法来执行Runnable接口或Callable接口中封装的线程任务。
- 使用线程池方式一:Runnable 接口
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
-
通过线程池创建工厂类(Executors),返回一个可尽快执行线程的线程池对象。
public static ExecutorService newFixedThreadPool(int nThreads) -
通过线程池对象的submit()方法,获取线程池中的某一个线程对象,并执行run方法。
Future<?> submit(Runnable task); -
使用线程池中线程对象的步骤:
-
通过Executors类获取一个线程池对象(ExecutorService)。
-
定义一个类并实现Runnable接口,并创建一个Runnable接口的实现类对象。
-
通过线程池对象(ExecutorService)的submit方法,提交 Runnable 接口子类对象。
-
调用线程池对象(ExecutorService)的shutdown()方法关闭线程池。
注意:调用submit方法后,即便线程任务执行完毕,程序并不终止,因为线程池控制了线程的关闭,将使用完的线程又归还到了线程池中,所以我们需要调用shutdown()方法来关闭线程池。
【示例】使用线程池方式之Runnable 接口
// Runnable接口实现类
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个助教");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("助教来啦:" + Thread.currentThread().getName());
System.out.println("助教帮我解决完问题后,继续回到助教的位置上面。");
}
}
// 测试类
public class ThreadDemo02 {
public static void main(String[] args) {
// 1.获取线程池对象,创建一个固定大小为2的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 2.创建Runnable实例对象
MyRunnable task = new MyRunnable();
// 3.调用线程池对象的submit方法,获取线程池中的某一个线程对象,并执行。
threadPool.submit(task);
threadPool.submit(task);
threadPool.submit(task);
// 4.关闭线程池,否则程序无法结束
threadPool.shutdown();
}
}
运行以上案例代码,输出结果如下:

我们创建一个固定大小为2的线程池,意味着可并发执行的任务数量最多为2个。程序中我们添加了3个线程任务,运行程序我们发现,“pool-1-thread-1”线程和“pool-1-thread-2”线程先执行其中两个任务,当“pool-1-thread-1”线程任务执行完毕之后,再由“pool-1-thread-1”线程执行第三个任务。
也就是说,使用线程池可以有效地控制系统中并发线程的数量,提高系统的运行性能,避免JVM崩溃的发生。
- 使用线程池方式二:Callable接口
Callable 接口与 Runnable 接口功能相似,用来指定线程的任务。其中的 call()方法,用来返回线程任务执行完毕后的结果,call 方法可抛出异常。
-
通过线程池对象的submit()方法,获取线程池中的某一个线程对象,并执行call方法。
<T> Future<T> submit(Callable<T> task); -
使用线程池中线程对象的步骤:
-
通过Executors类获取一个线程池对象(ExecutorService)。
-
定义一个类并实现Callable 接口,并创建一个Callable 接口的实现类对象。
-
通过线程池对象(ExecutorService)的submit方法,提交 Callable 接口子类对象。
-
调用线程池对象(ExecutorService)的shutdown()方法关闭线程池。
【示例】使用线程池方式之Callable 接口
// Callable 接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果
class MyCallable implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("我要一个助教");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("助教来啦:" + Thread.currentThread().getName());
System.out.println("助教帮我解决完问题后,继续回到助教的位置上面。");
return null;
}
}
// 测试类
public class ThreadDemo03 {
public static void main(String[] args) {
// 1.获取线程池对象,包含2个线程对象
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 2.创建Callable实例对象
MyCallable task = new MyCallable();
// 3.调用线程池对象的submit方法,获取线程池中的某一个线程对象,并执行。
threadPool.submit(task);
threadPool.submit(task);
threadPool.submit(task);
// 4.关闭线程池,否则程序无法结束
threadPool.shutdown();
}
}
以上案例中,通过Callable 的call方法来执行线程任务,实现的效果和Runnable接口一致。关于Callable接口的更深层次使用,我们后续章节中再细讲。
5.ScheduleExecutorService接口
在上面的五个方法中,后面两个方法的返回值都是一个ScheduleExecutorService对象,ScheduleExecutorService代表可延迟执行线程任务的线程池。
ScheduleExecutorService类是类的子类。所以,它里面也有直接提交任务的方法,并且新增了一些延迟任务处理的方法:

TimeUnit 表示给定单元粒度的时间段,它提供在这些单元中进行跨单元转换和执行计时及延迟操作的实用工具方法。TimeUnit 不维护时间信息,但是有助于组织和使用可能跨各种上下文单独维护的时间表示形式。毫微秒定义为千分之一微秒,微秒为千分之一毫秒,毫秒为千分之一秒,一分钟为六十秒,一小时为六十分钟,一天为二十四小时。
TimeUnit类中有7种静态属性:
TimeUnit.DAYS; // 天
TimeUnit.HOURS; // 小时
TimeUnit.MINUTES; // 分钟
TimeUnit.SECONDS; // 秒
TimeUnit.MILLISECONDS; // 毫秒
TimeUnit.MICROSECONDS; // 微妙
TimeUnit.NANOSECONDS; // 纳秒
接下来,我们通过实现ScheduleExecutorService类来延迟线程池案例。
使用延迟线程池中线程对象的步骤:
-
通过线程池创建工厂类(Executors),获取一个可延迟执行线程任务的线程池。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)public static ScheduledExecutorService newSingleThreadScheduledExecutor() -
通过延迟线程池对象的schedule()方法,获取延迟线程池中的某一个线程对象,并执行。
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) -
调用线程池对象(ExecutorService)的shutdown()方法关闭线程池。
【示例】使用延迟线程池案例
// Runnable接口实现类
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行...");
}
}
// 测试类
public class ThreadDemo02 {
public static void main(String[] args) {
// 1.获取线程池对象,包含2个线程对象
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
// 2.创建Runnable实例对象
MyRunnable task = new MyRunnable();
System.out.println("即将执行任务...");
// 3.调用线程池对象的schedule方法,获取线程池中的某一个线程对象,并执行。
pool.schedule(task, 2000, TimeUnit.MILLISECONDS);
pool.schedule(task, 2000, TimeUnit.MILLISECONDS);
// 4.关闭线程池,否则程序无法结束
pool.shutdown();
}
}
运行程序,可以明显看到,两个线程任务不是立即执行,而是延迟了2秒再执行的。
6.Future 接口
Future类位于java.util.concurrent包下,它也是一个接口,该接口就是用来记录Runnable或者Callable任务执行完毕后产生的结果(一般用于获取Callable任务执行完毕后的结果)。
必要时可以通过get方法获取线程任务执行的结果,该方法会产生阻塞,一直等到任务执行完毕才返回。

【示例】实现累加求和案例
// Callable接口实现类
class MyCallable implements Callable<Integer> {
private int number;
public MyCallable(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i < number; i++) {
sum += i;
}
return sum;
}
}
// 测试类
public class ThreadDemo {
public static void main(String[] args) {
// 1.获取线程池对象,包含2个线程对象
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 2.可以执行Runnable对象或者Callable对象代表的线程
Future<Integer> future1 = threadPool.submit(new MyCallable(10));
Future<Integer> future2 = threadPool.submit(new MyCallable(100));
// 3.通过Future的get方法,获取call方法执行完毕之后的结果
try {
Integer sum1 = future1.get();
Integer sum2 = future2.get();
System.out.println("sum1=" + sum1 + " sum2=" + sum2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 4.关闭线程池,否则程序无法结束
threadPool.shutdown();
}
}
执行以上程序,输出结果为:

ps:如需最新的免费文档资料和教学视频,请添加QQ群(627407545)领取。
本文详细介绍了Java中的线程池概念,解释了为何使用线程池能降低资源消耗,提高响应速度和线程的可管理性。讨论了Runnable与Callable接口的区别,以及如何通过ExecutorService接口和Executors类创建线程池。此外,还提到了ScheduleExecutorService接口用于延迟执行任务的功能,以及Future接口用于获取任务执行结果的重要性。
510

被折叠的 条评论
为什么被折叠?



