Java自定义线程池
线程池概念图
线程池的组成
简单聊一聊线程池:线程的多次重复创建需要消耗系统大量的资源,而线程池的做法是当需要大量的线程时,由线程池统一创建,交由main线程使用,且可重复使用,避免了创建大量的线程。
这里是一个最简单的线程池,由三部分组成:线程池,阻塞队列,main调用者。
线程池:用来创建线程,创建的线程用worker表示,用workers集合存储
参数:
任务队列:queue
线程集合:workers
核心数:coreSize(用来限制创建的worker数量,即只允许线程池创建多少个线程)
阻塞队列:用于两种情况:
1)当main调用者传来很多个任务(task)时,er线程池的worker不够用,这个时候这些task进入阻塞队列,开始等待,为了区分,这里将这种情况命名为Task阻塞
2)当线程池的worker有空闲时,所有main调用者的task全部被执行完毕,这时候还没有新的taskchuanlai,而worker却是多了的一方,这时候空闲的worker进入阻塞队列,同样为了区分,将这种情况命名为Worker阻塞
因此阻塞队列里至少有两种方法:Task阻塞,Worker阻塞
参数:
队列:queue
锁:lock(ReentrantLock)
生产者条件变量:fullWaitSet,当Task阻塞情况发生,而且阻塞队列满了的时候,应该进行等待,不能继续先阻塞队列添加task
消费者条件变量:emptyWaitSet,当Worker阻塞情况发生,即阻塞队列为空,且没有新的task传来时,应该让worker进行等待
容量上限:capcity,阻塞队列的容量上限
创建工程
接下来用代码进行演示
首先创造一个空的maven工程
引入两个jar
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
阻塞队列
创建一个类 TestPool(一个类就够了)
编写阻塞队列代码段(注意这是一个类,不是内部类),参数如下:
@Slf4j
class BlockQueue {
//任务队列
private Deque queue = new ArrayDeque<>();
//锁
private ReentrantLock lock = new ReentrantLock();
//生产者条件变量,等待的数量达到一定数目时,开始生产
private Condition fullWaitSet = lock.newCondition();
//消费者条件变量,
private Condition emptyWaitSet = lock.newCondition();
//容量上限
private int capcity;
public BlockQueue(int capcity) {
this.capcity = capcity;
}
}
接下编写阻塞队列的两个方法,即Task阻塞和Worker阻塞
Task阻塞方法,命名为take:
public T take(){
lock.lock();
try {
while (queue.isEmpty()){
try {
log.debug("队列为空");
//队列是否为空
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//移除
T t = (T) queue.removeFirst();
//唤醒生产者
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
解释一波:首先加锁,这个时候有个try代码块用来实现具体操作,还有一个finally代码块释放锁
然后再try代码块中:使用while循环判断阻塞队列是否为空,如果为空,日志打印,然后将emptyWaitSet标志为等待状态,因为这个时候阻塞队列里面没有task任务。
如果不为空,即代表阻塞队列里面有task任务,则移除队列里面的第一个,并且唤醒fullWaitSet。因为此刻移除了一个阻塞队列中的task,导致阻塞队列并不是满的,因此唤醒fullWaitSet可恶意继续向阻塞队列中添加task
Worker阻塞方法,命名为put:
public void put(T task){
lock.lock();
try {
//队列是否已经满了
while (queue.size()==capcity){
try {
log.debug("等待加入任务队列{}...",task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加
log.debug("加入任务队列{}",task);
queue.addLast(task);
//唤醒消费者
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
解释一波:首先加锁,这个时候有个try代码块用来实现具体操作,还有一个finally代码块释放锁
然后再try代码块中:判断阻塞队列里面的数量是否等于设置的容量上限,如果是,日志打印,然后将fullWaitSet标志为等待状态,因为这个时候阻塞队列已经满了,全是task任务,需要停下来,不能继续往阻塞队列里面添加task
如果不等于设置的容量上限,即代表还可以继续往阻塞队列里面添加task任务,则往里面添加,且代表阻塞队列至少有一个task(刚刚添加的),因此唤醒emptyWaitSet,让take方法继续执行
还有一个获取队列里面task数量的方法:
//获取大小
public int size(){
lock.lock();
try {
return queue.size();
}finally {
lock.unlock();
}
}
线程池
接下来编写线程池类,参数如下:
@Slf4j
class ThreadPool{
//任务队列
private BlockQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers=new HashSet();
//核心线程数
private int coreSize;
//获取任务的超时时间
private long timeout;
private TimeUnit unit;
}
构造方法:
public ThreadPool(int coreSize, long timeout, TimeUnit unit,int queueCapcity) {
this.coreSize = coreSize;
this.timeout = timeout;
this.unit = unit;
this.taskQueue=new BlockQueue<>(queueCapcity);
}
编写执行task任务的方法execute:
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接交给worker对象执行
//如果任务数超过coreSize时,加入任务队列暂存起来
synchronized (workers){
if (workers.size()<coreSize){
Worker worker = new Worker(task);
log.debug("新增worker{},{}",worker,task);
workers.add(worker);
worker.start();
}else {
taskQueue.put(task);
}
}
}
解释一波:首先传过来的参数是一个Runnable的task,就是一个任务
然后使用synchronized 锁对workers集合进行加锁,判断已有的worker数量(线程池已经创建的线程数量)是否小于设置的核心数,如果小于,则继续创建新的worker,并且加入workers集合中,并用这个新的worker来完成这个task任务
如果达到设置的线程数了,即代表没有空闲的worker了,则使用阻塞队列的对象(第一个引进来的对象)的put方法,将这个task任务放进阻塞队列暂存起来,后面步骤参考上面的解释
编写一个内部类Worker,继承Thread类
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//执行任务
//1)当task不为空,执行任务
//2)当task执行完毕,再从任务队列获取任务并执行
while (task!=null || (task=taskQueue.take()) !=null){
try {
log.debug("正在执行...{}",task);
task.run();
}catch (Exception e){
}finally {
task=null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
}
解释一波:重写runff,首先判断传过来的task是不是null或者阻塞队列里面还有没task,只要有,则执行run方法,并且执行完之后进这个task设为null,然后继续while循环,知道那两个条件都不能满足
如果都不满足则代表阻塞队列里面没有task任务且没有新的task传来,这时候循环结束,执行另外一个方法,将这个worker线程在workers集合中移除掉,加锁是为了防止在移除worker时,还有其他线程对workers集合进行操作
测试
线程池和阻塞队列都已经写好,接来进行测试,测试代码:
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS,2);
for (int i = 0; i < 4; i++) {
int j=i;
threadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",j);
});
}
}
}
解释一波:这里创建的线程池参数为:1个核心数,1000毫秒的等待(目前还没有),阻塞队列的大小2。意思就是阻塞队列里面只能塞两个task,线程池只能创建1个线程
另外main调用者(主函数)这里想要创建4个任务,而且每个任务要执行1秒钟(太快了没感觉),另外用j表示哪一个任务并用日志打印出来
控制台输出日志:
23:29:56.659 [main] DEBUG com.lzx.test.ThreadPool - 新增workerThread[Thread-0,5,main],com.lzx.test.TestPool$$Lambda$1/1869997857@1e81f4dc
23:29:56.662 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@9e89d68
23:29:56.662 [main] DEBUG com.lzx.test.BlockQueue - 等待加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32...
23:29:56.662 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@1e81f4dc
23:29:57.662 [Thread-0] DEBUG com.lzx.test.TestPool - 0
23:29:57.662 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@9e89d68
23:29:57.662 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32
23:29:57.662 [main] DEBUG com.lzx.test.BlockQueue - 等待加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d...
23:29:58.662 [Thread-0] DEBUG com.lzx.test.TestPool - 1
23:29:58.662 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32
23:29:58.662 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d
23:29:59.663 [Thread-0] DEBUG com.lzx.test.TestPool - 2
23:29:59.663 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d
23:30:00.663 [Thread-0] DEBUG com.lzx.test.TestPool - 3
23:30:00.663 [Thread-0] DEBUG com.lzx.test.BlockQueue - 队列为空
一行一行解释吧:
1.首先第一个任务进去,这时候线程池里面还没有创建线程,因此新创建了一个线程并执行这个任务
2.然后第二个任务进来,因为只有一个worker,所以没办法,只能进入阻塞队列暂存起来
3..第三个任务也进来了,这时候worker没空,阻塞队列又满了,因为设置阻塞队列的个数设置为2,而第一个和第二个正在里面,因此这时候将fullWaitSet设置为等待状态,不在往阻塞队列里面添加task
4.这是第一个任务正在执行,但是还没结束,快了
5.第一个任务执行完毕,并将这task移除掉,并且唤醒fullWaitSet
6.这时候立刻开始执行第二个任务
7.因为fullWaitSet被唤醒,所以第三个任务成功接入到阻塞队列中
8.第四个任务也想进来,但是没办法,阻塞队列满了,又将fullWaitSet设置为等待状态,不在往阻塞队列里面添加task
9.第二个任务执行完毕,并将这task移除掉,并且唤醒fullWaitSet
10.这时候立刻开始执行第三个任务
11.因为fullWaitSet被唤醒,所以第四个任务成功接入到阻塞队列中
12.第三个任务执行完毕,并将这task移除掉,并且唤醒fullWaitSet(尽管fullWaitSet并没有沉睡)
13.这时候立刻开始执行第四个任务
14.第四个任务执行完毕,并将这task移除掉,并且唤醒fullWaitSet(尽管fullWaitSet并没有沉睡)
15.因为阻塞队列里面的task为空且没有新的task传来,所以打印日志“队列为空”,且将emptyWaitSet设置为等待状态
代码优化
这里并没有设置超时等待,因此程序并没有结束,而是一直在等待
接下来改进代码
首先修改BlockQueue里面的两个阻塞方法:
Task阻塞
public T poll(long timeout, TimeUnit unit){
lock.lock();
try {
//将其他时间(timeout)统一转换为纳秒
long nanos = unit.toNanos(timeout);
//队列是否为空
while (queue.isEmpty()){
log.debug("超时队列为空");
try {
//剩余时间小于等于0,无需等待,直接返回
if (nanos<=0){
return null;
}
//如果等待途中被唤醒,则返回一个还需要等待的时间,即总时间-被唤醒时等待了的时间=新的时间(nanos)
nanos=emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//移除
T t = (T) queue.removeFirst();
//唤醒生产者
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
解释一波:大致没变,加入了时间概念。
首先判断超时时间是否小于0,小于0就直接return。
关键点在于这个地方:nanos=emptyWaitSet.awaitNanos(nanos); 因为有可能中途被唤醒,等待时间没到,不过这里并不会被唤醒,因为返回的nanos是一个差值,不到时间它会继续沉睡
Worker阻塞
public boolean offer(T task,long timeout,TimeUnit unit){
lock.lock();
try {
long nanos = unit.toNanos(timeout);
//队列是否已经满了
while (queue.size()==capcity){
try {
log.debug("等待加入任务队列{}...",task);
if (nanos<=0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加
log.debug("加入带超时的任务队列{}",task);
queue.addLast(task);
//唤醒消费者
emptyWaitSet.signal();
return true;
}finally {
lock.unlock();
}
}
没啥改变,和上面一样理解
设置等待一段时间后,程序停止,修改内部类里面的run方法:
@Override
public void run() {
//执行任务
//1)当task不为空,执行任务
//2)当task执行完毕,再从任务队列获取任务并执行
while (task!=null || (task=taskQueue.poll(timeout,unit)) !=null){
try {
log.debug("正在执行...{}",task);
task.run();
}catch (Exception e){
}finally {
task=null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
不修改main里面的参数,运行代码,控制台得到的日志:
23:51:28.418 [main] DEBUG com.lzx.test.ThreadPool - 新增workerThread[Thread-0,5,main],com.lzx.test.TestPool$$Lambda$1/1869997857@1e81f4dc
23:51:28.421 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@9e89d68
23:51:28.421 [main] DEBUG com.lzx.test.BlockQueue - 等待加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32...
23:51:28.421 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@1e81f4dc
23:51:29.421 [Thread-0] DEBUG com.lzx.test.TestPool - 0
23:51:29.421 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@9e89d68
23:51:29.421 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32
23:51:29.421 [main] DEBUG com.lzx.test.BlockQueue - 等待加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d...
23:51:30.422 [Thread-0] DEBUG com.lzx.test.TestPool - 1
23:51:30.422 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@3b192d32
23:51:30.422 [main] DEBUG com.lzx.test.BlockQueue - 加入任务队列com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d
23:51:31.422 [Thread-0] DEBUG com.lzx.test.TestPool - 2
23:51:31.422 [Thread-0] DEBUG com.lzx.test.ThreadPool - 正在执行...com.lzx.test.TestPool$$Lambda$1/1869997857@311d617d
23:51:32.422 [Thread-0] DEBUG com.lzx.test.TestPool - 3
23:51:32.422 [Thread-0] DEBUG com.lzx.test.BlockQueue - 超时队列为空
23:51:33.423 [Thread-0] DEBUG com.lzx.test.BlockQueue - 超时队列为空
23:51:33.423 [Thread-0] DEBUG com.lzx.test.ThreadPool - worker被移除Thread[Thread-0,5,main]
Process finished with exit code 0
这里就不再解释了,可以看到的是代码在等待了一段时间之后自己停止了
拒绝策略
再进行优化,加入拒绝策略
在之前的代码中,当面对没有task任务时,第一种处理方式是死等,线程就一直运行;第二种处理方式是等待一段时间,然后自己结束。
两种不同的方式需要不同的代码,因此并不采纳,这里采用拒绝策略,让我们在不修改核心代码的前提下自己修改处理方式。
首先在类里面添加一个接口
@FunctionalInterface//拒绝策略
interface RejectPolicy<T>{
void reject(BlockQueue queue,T task);
}
线程池里面添加一个参数,修改如下:
@Slf4j
class ThreadPool{
//任务队列
private BlockQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers=new HashSet();
//核心线程数
private int coreSize;
//获取任务的超时时间
private long timeout;
private TimeUnit unit;
private RejectPolicy<Runnable> rejectPolicy;
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接交给worker对象执行
//如果任务数超过coreSize时,加入任务队列暂存起来
synchronized (workers){
if (workers.size()<coreSize){
Worker worker = new Worker(task);
log.debug("新增worker{},{}",worker,task);
workers.add(worker);
worker.start();
}else {
//此方法位死等,用新方法,可自己管理策略
// taskQueue.put(task);
//让调用者自己执行
taskQueue.tryPut(rejectPolicy,task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit unit,int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.unit = unit;
this.taskQueue=new BlockQueue<>(queueCapcity);
this.rejectPolicy=rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//执行任务
//1)当task不为空,执行任务
//2)当task执行完毕,再从任务队列获取任务并执行
while (task!=null || (task=taskQueue.poll(timeout,unit)) !=null){
try {
log.debug("正在执行...{}",task);
task.run();
}catch (Exception e){
}finally {
task=null;
}
}
synchronized (workers){
log.debug("worker被移除{}",this);
workers.remove(this);
}
}
}
}
在阻塞队列BlockQueue类中添加方法:
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//判断队列是否满
if (queue.size()==capcity){
rejectPolicy.reject(this,task);
}else {
//有空闲
//添加
log.debug("加入任务队列{}",task);
queue.addLast(task);
//唤醒消费者
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
main方法中:
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1,(queue,task)->{
//死等
queue.put(task);
//2)带超时等待
// queue.offer(task,1500,TimeUnit.MILLISECONDS);
//3)让调用者放弃任务执行
// log.debug("放弃{}",task);
//4)让调用者抛出异常
// throw new RuntimeException("任务执行失败 "+task);
//5)让调用者自己执行
// task.run();
});
for (int i = 0; i < 4; i++) {
int j=i;
threadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",j);
});
}
}
}
列举了5种策略,可自行选择