线程池原理
原理图
原理
线程池中包含一个队列(容量有限制)和创建线程池时必须指定的存在核心线程数量(如:上图中核心线程数量是3)。当任务的生产者需要把产生的任务交给线程池,当线程池中的核心线程有空闲时,直接创建线程并把任务交给创建的线程处理。如果核心线程的数量已经达到了3个(这里的3指的是创建线程池时执行的核心线程数量)且均在执行自己的任务,则把任务添加到队列中,等待核心线程执行完任务之后再从队列中获取任务(图中的poll)并执行。此时,如果核心线程均在忙且队列已满,那么线程池将无法容纳下更多的线程。这时候,不同种类的线程池出现了不同的处理方式(暂时先不讨论)。
自定义一个自己的线程池
使用过线程池的都知道,线程池是一个类。这里其中组合了一个队列。那么我们先创建这个队列类。
创建线程池中的容器–队列
容器这个类包含的属性应该有:容量,锁(保证并发安全),往容器中添加和取出任务时的条件变量,真正用来存储的队列(双向链表)。行为应该有从容器中取任务和添加任务。
容器类代码如下:
@Slf4j
public class BlockQueue<T> {
/**
* 双向链表--用于存放任务到队列中
*/
private Deque<T> arrayQueue = new ArrayDeque<>();
/**
* 用于向队列中添加或者移除元素时的线程安全
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 向队列中添加元素时保证队列中有空余
*/
private Condition putCondition = lock.newCondition();
/**
* 从队列中获取元素时保证元素不为空
*/
private Condition pollCondition = lock.newCondition();
/**
* 队列容量
*/
private int capacity;
public BlockQueue(int capacity) {
this.capacity = capacity;
}
/**
* 往队列中添加元素方法
* @param task
*/
public void put(T task) {
lock.lock();
try{
//使用while避免虚假唤醒
while (capacity == arrayQueue.size()) {
log.info("当前队列已满,添加任务被阻。。。");
try {
putCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("添加任务:{}到队列中",task);
arrayQueue.addLast(task);
pollCondition.signal();
} finally {
lock.unlock();
}
}
/**
* 从队列中获取一个任务
* @return
*/
public T poll() {
lock.lock();
try{
while (arrayQueue.isEmpty()) {
log.info("当前队列中无任务。。。");
try {
pollCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = arrayQueue.removeFirst();
putCondition.signal();
return t;
}finally {
lock.unlock();
}
}
public void tryPut(T task,RejectPolicy rejectPolicy) {
lock.lock();
try{
if (arrayQueue.size() < capacity) {
arrayQueue.addLast(task);
pollCondition.signal();
}else {
//拒绝策略,不同类型线程池均有不同的实现。
rejectPolicy.reject(this,task);
}
}finally {
lock.unlock();
}
}
}
创建线程池类
线程池中的容器创建完成之后,代表着核心线程的数量已经达到了3个(这里的3指的是创建线程池时执行的核心线程数量)且均在执行自己的任务情况下把任务添加到容器中事情已经做完。那么线程池类需要完成的则是线程池中的线程数量未达到指定的核心数量时,直接创建线程并执行任务。
线程池类包含的属性应该有容器类、核心线程数量,当前正在工作的线程数量。包含的行为是执行任务
线程池类代码如下
@Slf4j
public class MyThreadPool {
private BlockQueue<Runnable> blockQueue;
private int coreSize;
private RejectPolicy<Runnable> rejectPolicy;
private Set<Thread> workers = new HashSet<>();
public MyThreadPool(int blockQueueSize, int coreSize, RejectPolicy<Runnable> rejectPolicy) {
this.blockQueue = new BlockQueue<>(blockQueueSize);
this.coreSize = coreSize;
this.rejectPolicy = rejectPolicy;
}
public void execute(Runnable task) {
synchronized (workers){
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.info("创建新线程:{}",worker);
worker.start();
workers.add(worker);
}else {
//核心线程数不够,各家线程池均有自己的实现
blockQueue.tryPut(task,rejectPolicy);
}
}
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
while(task != null || (task = blockQueue.poll())!=null) {
try{
System.out.println("任务被执行"+task);
task.run();
}finally {
task = null;
}
}
synchronized (workers) {
workers.remove(this);
}
}
}
}
线程池类说明
1、execute方法实现依据写在了原理部分。
2、在线程池类中使用了一个内部类,这个内部类完成的工作就是task不为空(代表容器队列中有任务)就执行task。task为空时则把当前的线程移除。因为已经没有任务在等待被执行,避免浪费资源所以把多余线程释放掉。
为什么要是用内部类呢?
试想一下,如果不是使用内部类,那么怎么把空余线程从workers中移除。
测试类
创建线程池的第三个参数就是拒绝策略的具体实现,由于可以有多种拒绝策略,避免代码中出现较多的if/else所以由构造线程池时传入具体的实现方式。这里的拒绝策略使用的是让生产者自己执行该任务。
public class Test {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(4, 1, (queue,task) -> {
System.out.println("任务被主线程执行");
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
myThreadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行任务"+j);
});
}
}
}
有问题欢迎指正,1762353437@qq.com