JDK源码学习04-手撸一个简易线程池

本文介绍了如何手写一个简易线程池,通过Java的并发工具实现生产者消费者模型。线程池包含工作线程管理和任务队列,允许动态调整线程数量。文章还展示了如何添加关闭、增删线程及查看任务队列大小等功能,并提供了测试用例。通过此简易线程池,读者可以深入理解线程池的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDK源码学习04-手撸一个简易线程池

简介

本文实现的线程池非常简陋,只要稍有并发基础即可看懂(只要会生产者消费者即可),实现线程池非常类似于wait和notify实现的单生产者多消费者案例,基本上是一模一样。本文会给简陋的线程池依次添加一些功能函数,这才是比较值得重点阅读的。除此之外,在Woker中的run方法也非常值得学习,即类似下面这个框架结构,在JUC中也是频繁使用来实现同步,即在死循环中根据先cas尝试,尝试失败则进入阻塞,如果被唤醒,就会再次cas尝试,直到成功后执行处理逻辑。

# 使用synchronized实现同步
while(true){
	synchronized(核心资源(队列)){
		while(条件)
			wait();
	}
	。。。 // 处理逻辑
}

在这里插入图片描述

先撸一个破烂线程池

  1. 或许许多人一听线程池,就会觉得很难,并且非常不了解,笔者也是,现在也处在入门阶段。但是看Javav并发编程的艺术一书,发现原来最简单的线程池非常容易理解。
  2. 我们可以阅读以下代码,基本上有手就行。一开始是定义了线程池的基础工作线程的数量,其次使用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. 上述代码只有以下几个小重点
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一定要阻塞,但是阻塞在什么对象上呢?只能阻塞在资源对象上。
  1. 测试运行以上线程池
    我们设置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任务队列
创建线程池,初始化了两个线程被初始化然后阻塞被初始化然后阻塞
提交任务1execute方法中的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完毕执行任务1while循环再次检查任务队列,有任务就消费,无任务就阻塞,发现阻塞队列不为空 ,就取出并执行任务3任务3被消费,任务队列变成空
最终状态执行任务1执行任务3

下面我们在上述线程池上进行一些优化

  1. 我们定义一个接口,以便有一个规范
public interface IThreadPool{
    void execute(Runnable job);
    // 关闭线程池
    void shutdown();
    // 动态增加线程  提高线程池性能
    void addWorker(int num);
    // 动态删除线程  减少资源使用
    void removeWorker(int num);
    // 查看还有多少任务在等待被消费
    int getJobSize();
}
  1. 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;
        }
    }
  1. 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;
        }
    }
  1. shutdown实现
    @Override
    public void shutdown() {
        for (Worker worker : workers) {
            worker.shutdown();
        }
    }

祭出我珍藏的图,留待日后完善源码阅读

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值