对 Java 并发编程有种情节,就是希望把它弄清楚。继续...
什么是线程池?
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
为什么要使用线程池?
因为 Java 中创建一个线程,需要调用操作系统内核的 API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
使用线程池就能很好地避免频繁创建和销毁。
线程池是一种生产者——消费者模式
先看下一个简单的 Java 线程池的代码
package constxiong.concurrency.a010;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
/**
* 简单的线程池
* @author ConstXiong
*/
public class ThreadPool {
//阻塞队列实现生产者-消费者
BlockingQueue<Runnable> taskQueue;
//工作线程集合
List<Thread> threads = new ArrayList<Thread>();
//线程池的构造方法
ThreadPool(int poolSize, BlockingQueue<Runnable> taskQueue) {
this.taskQueue = taskQueue;
//启动线程池对应 size 的工作线程
for (int i = 0; i < poolSize; i++) {
Thread t = new Thread(() -> {
while (true) {
Runnable task;
try {
task = taskQueue.take();//获取任务队列中的下一个任务
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
threads.add(t);
}
}
//提交执行任务
void execute(Runnable task) {
try {
//把任务方法放到任务队列
taskQueue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池的使用测试
package constxiong.concurrency.a010;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 测试线程池的使用
* @author ConstXiong
*/
public class TestThreadPool {
public static void main(String[] args) {
// 创建有界阻塞任务队列
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10);
// 创建 3个 工作线程的线程池
ThreadPool tp = new ThreadPool(3, taskQueue);
//提交 10 个任务
for (int i = 1; i <= 10; i++) {
final int j = i;
tp.execute(() -> {
System.out.println("执行任务" + j);
});
}
}
}
打印结果
执行任务1
执行任务2
执行任务3
执行任务6
执行任务5
执行任务4
执行任务8
执行任务7
执行任务10
执行任务9
这个线程池的代码中
poolSize 线程池工作线程的个数
BlockingQueue taskQueue 用有界阻塞队列存储 Runnable 任务
execute(Runnable task) 提交任务
线程池对象被创建,就自动启动 poolSize 个工作线程
工作线程一直从任务队列 taskQueue 中取任务
线程池的原理就是这么简单,但是 JDK 中的线程池的功能,要远比这个强大的多。
JDK 中线程池的使用
JDK 中提供的最核心的线程池工具类 ThreadPoolExecutor,在 JDK 1.8 中这个类最复杂的构造方法有 7 个参数。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池保有的最小线程数。
maximumPoolSize:线程池创建的最大线程数。
keepAliveTime:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
unit:keepAliveTime 的时间单位
workQueue:任务队列
threadFactory:线程工厂对象,可以自定义如何创建线程,如给线程指定name。
handler:自定义任务的拒绝策略。线程池中所有线程都在忙碌,且任务队列已满,线程池就会拒绝接收再提交的任务。handler 就是拒绝策略,包括 4 种(即RejectedExecutionHandler 接口的 4个实现类)。
AbortPolicy:默认的拒绝策略,throws RejectedExecutionException
CallerRunsPolicy:提交任务的线程自己去执行该任务
DiscardPolicy:直接丢弃任务,不抛出任何异常
DiscardOldestPolicy:丢弃最老的任务,加入新的任务
JDK 的并发工具包里还有一个静态线程池工厂类 Executors,可以方便地创建线程池,但是由于 Executors 创建的线程池内部很多地方用到了无界任务队列,在高并发场景下,无界任务队列会接收过多的任务对象,导致 JVM 抛出OutOfMemoryError,整个 JVM 服务崩溃,影响严重。所以很多公司,尤其互联网大厂已经不建议使用 Executors 去创建线程。
Executors 的简介
虽然不建议使用,作为对 JDK 的学习,还是简单介绍一下.
newFixedThreadPool:创建定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程
newCachedThreadPool:创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制
newScheduledThreadPool:创建定长线程池,可执行周期性的任务
newSingleThreadExecutor:创建单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
newSingleThreadScheduledExecutor:创建单线程可执行周期性任务的线程池
newWorkStealingPool:任务可窃取线程池,不保证执行顺序,当有空闲线程时会从其他任务队列窃取任务执行,适合任务耗时差异较大。
源码:
https://github.com/ConstXiong/concurrency
Executors 具体使用下一篇继续研究。