线程池它就是一个池子(就像是养鱼的池子),可以养一定数量的鱼,可以重复使用!学习Java中的线程池,就是学习Java用了什么工具(API)和方法(设计模式)来搞出可以“养鱼的池子”。
本文作为入门级的线程池教程,主要介绍第一个线程池的一般写法,也就是“Hello,world”的水平,快速入门!
Table of Contents
什么是线程池
线程池体系
第一个线程池
什么是线程池
背景:
- 如果反复创建销毁线程,也会有一定的系统资源开销;
- 线程池,其实就是一个容纳多个线程的容器;Java初始时提供一定数量的、可重复使用的线程;
- 省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 此外,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。
为什么要使用线程池?
- 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。
- 在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。
- 如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。
- 为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
- 线程池主要用来解决线程生命周期开销问题和资源不足问题。
- 通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。
- 这样,就可以立即为请求服务,使用应用程序响应更快。
- 另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
注意:
- 程序一开始的时候,创建多个线程对象,存储到集合中;当需要线程的时候,从几回合中获取线程出来;
- 从JDK1.5开始,程序员不再需要自己开发线程池,而是使用内置线程池技术;
线程池体系
JDK给我们提供了Excutor框架来使用线程池,它是线程池的基础。Executor提供了一种将“任务提交”与“任务执行”分离开来的机制(解耦)。
第一个线程池
线程池的使用流程:
- 调用Executors类的静态方法创建线程池;
- 调用submit提交Runnable或Callable对象;
- 保存好返回的Future对象,以便得到结果或者取消任务;
- 当不想再提交任何任务时,调用shutdown
向线程池中提交Runnable接口的线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo {
public static void main(String[] args) {
//第一步:调用Executors类的静态方法创建线程池;
ExecutorService es = Executors.newFixedThreadPool(5); //创建一个固定大小的线程池
//第二步-1:Runnable或Callable对象
MyTask task = new MyTask();
//第二步-2:提交Runnable或Callable对象到线程池中
for(int i=0; i<10; i++) {
es.submit(task); //连续提交10个线程到线程池中
}
es.shutdown();
}
}
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+";线程执行ID:"+
Thread.currentThread().getName());
try {
Thread.sleep(1000); //休眠1s
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
程序输出:
- 时间:1585382666365;线程执行ID:pool-1-thread-1
- 时间:1585382666365;线程执行ID:pool-1-thread-4
- 时间:1585382666365;线程执行ID:pool-1-thread-5
- 时间:1585382666365;线程执行ID:pool-1-thread-2
- 时间:1585382666365;线程执行ID:pool-1-thread-3
- 时间:1585382667366;线程执行ID:pool-1-thread-5
- 时间:1585382667367;线程执行ID:pool-1-thread-1
- 时间:1585382667367;线程执行ID:pool-1-thread-2
- 时间:1585382667367;线程执行ID:pool-1-thread-3
- 时间:1585382667367;线程执行ID:pool-1-thread-4
分析:
- 根据输出:前5个任务和后5个任务的执行时间正好相差1s!
- 并且,前5个和后5个任务的线程ID都是完全一致的!说明前5个线程是同一个整体,后5个线程又是另一个整体!
向线程池中提交Callable接口的线程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadDemo {
public static void main(String[] args) {
//第一步:调用Executors类的静态方法创建线程池;
ExecutorService es = Executors.newFixedThreadPool(2); //创建一个固定大小的线程池
//第二步-1:创建Runnable或Callable对象
MyTask task = new MyTask();
//第二步-2:提交Runnable或Callable对象到线程池中
//第三步:用Future保存好每个任务,以便对任务的操作
Future<String> re01 = es.submit(task);
Future<String> re02 = es.submit(task);
try {
System.out.println(re01.get()+re02.get()); //对任务进行操作!
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//第四步:关闭线程池
es.shutdown();
}
}
class MyTask implements Callable<String> {
public String call() {
String info = Thread.currentThread().getName();
return info+"执行了....";
}
}
Callable接口和Runnable接口一样,都是Java中多线程的实现方式。其中Callable接口实现的线程可以抛出异常,也可以有返回值,而Runnable接口实现的线程则不可以(不可抛异常,不可有返回值)!