JDK源码学习04-手撸一个简易线程池
简介
本文实现的线程池非常简陋,只要稍有并发基础即可看懂(只要会生产者消费者即可),实现线程池非常类似于wait和notify实现的单生产者多消费者案例,基本上是一模一样。本文会给简陋的线程池依次添加一些功能函数,这才是比较值得重点阅读的。除此之外,在Woker中的run方法也非常值得学习,即类似下面这个框架结构,在JUC中也是频繁使用来实现同步,即在死循环中根据先cas尝试,尝试失败则进入阻塞,如果被唤醒,就会再次cas尝试,直到成功后执行处理逻辑。
# 使用synchronized实现同步
while(true){
synchronized(核心资源(队列)){
while(条件)
wait();
}
。。。 // 处理逻辑
}
先撸一个破烂线程池
- 或许许多人一听线程池,就会觉得很难,并且非常不了解,笔者也是,现在也处在入门阶段。但是看Javav并发编程的艺术一书,发现原来最简单的线程池非常容易理解。
- 我们可以阅读以下代码,基本上有手就行。一开始是定义了线程池的基础工作线程的数量,其次使用AtomicLong在创建线程的时候给线程一个id即可。
public class MyThreadPool2{
private static final int MAX_WORKER_SIZE = 10;
private static final int MIN_WORKER_SIZE = 1;
private static final int DEFAULT_WORKER_SIZE = 5;
// 线程编号
private AtomicLong threadNum = new AtomicLong();
// 任务队列 workers醒来的时候会加锁并尝试获取任务并执行
private static final LinkedList<Runnable> jobs = new LinkedList<>();
private static final List<Worker> workers = Collections.synchronizedList(new ArrayList<Worker>());
private int workNum;
public MyThreadPool2() {
this(DEFAULT_WORKER_SIZE);
}
public MyThreadPool2(int workNum) {
this.workNum = Math.min(MAX_WORKER_SIZE, Math.max(workNum, MIN_WORKER_SIZE));
for (int i = 0; i < this.workNum; i++) {
Worker worker = new Worker();
workers.add(worker);
new Thread(worker, "MyThreadPool--worker-->" + threadNum.getAndIncrement()).start();
}
workNum += this.workNum;
}
public void execute(Runnable runnable) {
if (jobs == null)
return;
synchronized (jobs) {
jobs.add(runnable);
// 只唤醒任意一个在jobs上等待的worker线程即可
jobs.notify();
}
}
public static class Worker implements Runnable {
private volatile boolean isRunning = true;
@Override
public void run() {
while (isRunning) {
Runnable job = null;
synchronized (jobs) {
// 防止虚假唤醒 即线程wait后 莫名自己醒来
while (jobs.isEmpty()) {
try {
jobs.wait();
} catch (InterruptedException e) {
// 外部调用interrupted之后 wait中的本线程会排除异常
// 然后本线程的中断标识位被置为false 然而对外需要辨识本线程已经中断 顾再次调用
Thread.interrupted();
return;
}
}
// 此时worker从等待中被唤醒 并成功获取到锁 按理说jobs不可能没有任务
// 但笔者对并发熟练度不高 仍旧使用不抛出异常的poll 并判空
job = jobs.pollFirst();
}
if (job != null) {
try {
job.run();
} catch (Exception e) {
// 线程可能会抛出异常
// e.printStackTrace();
}
}
}
}
public void shutdown() {
isRunning = false;
}
}
}
- 上述代码只有以下几个小重点
1. 构造函数中,先创建好worker线程,worker线程在任务队列没有元素时会阻塞execute函数
会向任务队列提供任务(由使用线程池的用户手动添加任务)
2. worker线程的run方法详细解读:
a. 首先一个死循环循环isRunning变量,用来控制工作线程的关闭(如果线程job.run()方法
是死循环就关闭不掉,因为执行不到检查变量的while循环了),
b. 其实这就是一个生产者消费者的案例,不过是资源为jobs的工作队列,
由用户手动作为生产者,而worker作为消费者,如果有用户生产一个任务出来,就会通知任意一个woker前来消费
c. 每个worker消费完成之后,如果任务队列不为空就执行下一个任务,否则继续阻塞。
3. 为什么在关键方法中锁的是jobs?
因为jobs是关键资源,并且一切都是jobs引起的,worker线程工作需要从jobs中获取,多个线程操作jobs是不安全的,
并且很明显,worker一定要阻塞,但是阻塞在什么对象上呢?只能阻塞在资源对象上。
- 测试运行以上线程池
我们设置MyThreadPool2的线程数为2,然后开启了三个线程,第一个不断循环打印,第二个只循环打印两次就结束了,第三个仍然不断循环打印
public static void main(String[] args) throws InterruptedException {
MyThreadPool2 pool = new MyThreadPool2(2);
pool.execute(() -> {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行了任务" + 1);
}
});
pool.execute(() -> {
for (int i = 0; i < 2; i++){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行了任务" + 2);
}
});
pool.execute(() -> {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行了任务" + 3);
}
});
}
输出
MyThreadPool--worker-->0 执行了任务1
MyThreadPool--worker-->1 执行了任务2
MyThreadPool--worker-->1 执行了任务2
MyThreadPool--worker-->0 执行了任务1
MyThreadPool--worker-->0 执行了任务1
MyThreadPool--worker-->1 执行了任务3
正在发生的事件 | 线程0 | 线程1 | 任务队列 |
---|---|---|---|
创建线程池,初始化了两个线程 | 被初始化然后阻塞 | 被初始化然后阻塞 | 空 |
提交任务1 | execute方法中的notify唤醒了线程0(可能唤醒线程0也可能唤醒线程1)执行任务1:不断睡眠打印 | 阻塞ing | 先添加然后被消费,最后为空 |
提交任务2 | 执行任务1的睡眠中 | 只剩下线程1能被唤醒执行任务,顾线程1醒来执行任务2: 睡眠打印两次 | 先添加然后被消费,最后为空 |
提交任务3 | 执行任务1 的睡眠中 | 执行任务2的睡眠中 | 此时没有空闲线程,不会被消费,所以任务队列会存储任务3,等待线程0或者1空闲的时候执行 |
上述操作都会在短时间内完成 | 执行任务1 | 执行任务2 | 任务3待消费 |
当第一个两秒钟到达的时候 | 执行任务1打印语句并接着睡眠 | 执行任务2打印语句并接着睡眠 | 任务3待消费 |
当第二个两秒钟到达的时候 | 执行任务1打印语句并接着睡眠 | 执行任务2打印语句并结束 | 任务3待消费 |
下述操作会在短时间内完成 | 执行任务1 | 任务2执行完毕 | 任务3待消费 |
线程1执行任务2完毕 | 执行任务1 | while循环再次检查任务队列,有任务就消费,无任务就阻塞,发现阻塞队列不为空 ,就取出并执行任务3 | 任务3被消费,任务队列变成空 |
最终状态 | 执行任务1 | 执行任务3 | 空 |
下面我们在上述线程池上进行一些优化
- 我们定义一个接口,以便有一个规范
public interface IThreadPool{
void execute(Runnable job);
// 关闭线程池
void shutdown();
// 动态增加线程 提高线程池性能
void addWorker(int num);
// 动态删除线程 减少资源使用
void removeWorker(int num);
// 查看还有多少任务在等待被消费
int getJobSize();
}
- addWorker实现
@Override
public void addWorker(int num) {
synchronized (jobs) {
num = Math.min(num, MAX_WORKER_SIZE - num);
for (int i = 0; i < num; i++) {
Worker worker = new Worker();
workers.add(worker);
new Thread(worker, "MyThreadPool--worker-->" + threadNum.getAndIncrement()).start();
}
workNum += num;
}
}
- removeWorker实现
– 需要注意的是如果移除worker,不会影响worker正在执行的任务。
@Override
public void removeWorker(int num) {
synchronized (jobs) {
if (num >= workNum)
throw new RuntimeException("线程池至少得有一个线程...");
int count = 0;
while (count < num) {
Worker worker = workers.get(0);
if (workers.remove(worker)) {
worker.shutdown();
count++;
}
}
this.workNum -= count;
}
}
- shutdown实现
@Override
public void shutdown() {
for (Worker worker : workers) {
worker.shutdown();
}
}