并发编程系列学习笔记07(线程池)

线程池

  • 问题背景

    • 线程资源很宝贵
    • 线程不是越多越好
    • 频繁创建线程影响性能
  • 自定义线程池

    • 消费者线程池:Thread Pool
    • 生产者任务队列:Blocking Queue
/**
 * @author 钦尘
 * @date 2021/8/3 23:10
 * @description 手写自定义线程池
 */
@Slf4j
public class TestPool {

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadPool threadPool = new ThreadPool(2, 1000, TimeUnit.MICROSECONDS, 10);

        for (int i = 0; i < 5; i++) {
            int j = i;
            threadPool.execute(() -> {
                log.info("执行任务逻辑 {}", j);
            });
        }

    }

}

@Slf4j
class ThreadPool {

    /**
     * 任务队列
     */
    private BlockingQueue<Runnable> taskQueue;

    /**
     * 线程集合
     */
    private HashSet<Worker> workers = new HashSet();

    /**
     * 核心线程数
     */
    private int coreSize;

    /**
     * 获取任务的超时时间
     */
    private long timeout;

    /**
     * 时间单位
     */
    private TimeUnit timeUnit;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
    }

    /**
     * 执行任务
     *
     * @param task
     */
    public void execute(Runnable task) {
        // 当任务数 未超过 coreSize 直接交给 worker对象执行
        // 达到数量,加入队列
        synchronized (workers) {
            if (workers.size() < coreSize) {
                log.info("新增任务 {}", task);
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start();
            } else {
                log.info("加入任务队列 {}", task);
                taskQueue.put(task);
            }
        }
    }

    /**
     * 对线程进行包装
     */
    class Worker extends Thread {

        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 执行任务
            // 1.当 task 不为 null 直接执行
            // 2.当 task执行完毕,从任务队列获取任务,执行
            // 测试不带超时 和 带超时的 区别,两种不同策略,一种是死等,一种是一段时间后放弃等待,线程销毁
            // while (task != null || (task = taskQueue.take()) != null) {
            while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.info("任务正在执行 {}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.info("任务执行完毕,被移除 {}", this);
                workers.remove(this);
            }
        }
    }

}

/**
 * 阻塞队列,存放线程未处理的任务
 *
 * @param <T>
 */
@Slf4j
class BlockingQueue<T> {

    /**
     * 任务队列,使用双向链表,ArrayDeque比LinkedList性能更好
     */
    private Deque<T> queue = new ArrayDeque<>();

    /**
     * 锁,保护队列头尾元素
     */
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 队列最大容量
     */
    private int capcity;

    /**
     * 生产者条件变量
     */
    private Condition fullWaitSet = lock.newCondition();

    /**
     * 消费者条件变量
     */
    private Condition emptyWaitSet = lock.newCondition();

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    /**
     * 阻塞获取,不带超时时间
     * @return 任务
     */
    public T take() {
        lock.lock();
        try {
            // 队列为空,则进入等待
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 拿到任务,从队列中移除,并返回该队列,同时通知 条件变量 fullWaitSet
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 带超时的阻塞获取
     *
     * @param timeout
     * @param unit
     * @return
     */
    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                if (nanos <= 0) {
                    return null;
                }
                try {
                    // 有可能出现虚假唤醒,所以将返回的是剩余时间再次赋给 nanos
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 取出元素,并将元素从队列中移除
            T task = queue.removeFirst();
            // 唤醒生产者线程可继续放入任务
            fullWaitSet.signal();
            return task;
        } finally {
            lock.unlock();
        }
    }

    public void put(T element) {
        lock.lock();
        try {
            // 如果当前任务队列容量已经满了,则让其进入等待状态
            while (queue.size() >= capcity) {
                try {
                    log.info("任务队列已满,等待加入任务队列", element);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 队列有空闲空间,添加任务到队列尾部,并通知消费者线程开始处理
            log.info("任务加入到队列尾部 {}", element);
            queue.addLast(element);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }
}
  • ThreadPoolExeutor

    • 线程池的五种状态

      • 采用int高3位存储线程池状态,低29位标识线程数量
      • 存储在一个原子变量clt中的原因:状态与数量合二为一,一次CAS原子操作即可完成赋值
      • Running 、SHUTDOWN、STOP、TIDYING、TERMINATED
      • 了解SHUTDDOWN 与 STOP区别
    • 7个构造方法参数

      • corePoolSize -> 核心线程数
      • maximumPoolSize -> 允许的最大线程数
      • keepAliveTime -> 当线程数大于核心数时,多余的空闲线程在终止前等待新任务的最长时间
      • unit -> 时间单位
      • workQueue -> 执行任务之前保存任务的队列
      • threadFactory -> 创建新线程时使用的工厂
      • rejectedExecutionHandler -> 达到线程边界和队列容量而阻塞执行时要使用的处理程序
    • 核心线程与救急线程

      • 核心+救急=最大
      • 当核心线程都在忙,任务先进入队列
      • 阻塞队列中放不下时,就会让救急线程去处理
      • 高峰期过后,救急线程执行完任务,就会被销毁
      • 核心线程不会销毁
      • 核心线程与救急线程都繁忙,此时执行拒绝策略
      • 救急线程的前提是配合有界队列实现
    • JDK提供的4种拒绝策略

      • 抛出异常(默认)
      • 让调用者运行任务
      • 放弃任务
      • 让队列中最早放入队列的任务,本任务取而代之
    • 第三方拒绝策略扩展

      • Dubbo:异常前会dump线程信息,方便定位问题
      • Netty:创建新的线程来执行
      • ActiveMQ:带超时等等,再重试
      • PinPoint:逐一阐释策略链中各类拒绝策略
    • newFixedThreadPool

      • 创建固定大小线程池,核心=最大,没有救急
      • 有默认线程工厂实现:DefaultThreadFactory,也可自定义
      • 无界队列,可放任意数量任务
      • 适合线程数量很明确场景
    • newCachedThreadPool

      • 创建带缓冲的线程池
      • 创建的线程全部都是救急线程
      • 底层基于SynchronousQueue无容量队列实现
      • 线程数根据任务量数量增长而增长
      • 适合任务数量较密集但任务执行时间较短业务
    • newSingleThreadExecutor

      • 单线程池执行器,线程数固定为1
      • 适合希望多个任务排队执行业务
      • 比起我们自己创建一个线程执行任务更安全可靠
      • 对比固定线程池FTP传1,STE进行外层装饰,只暴露基本能力,使用更安全,严格
    • 提交任务

      • execute

      • 支持执行带有返回结果任务:submit

      • invokeAll

        • 执行提交的所有任务
      • invokeAny

        • 找到最先执行的任务,执行完即算完
    • 关闭线程池

      • shutdown

        • 状态变为:shutdown
        • 线程池会再接受新的任务
        • 已提交的任务会继续执行完
        • 不阻塞调用shutdown方法线程的后续逻辑执行
      • shutdownNow

        • 不会接受新的任务
        • 已提交任务会返回,可自行处理
        • 正在执行的线程会被打断
        • 可以立即终结
      • isShutdown

      • isTerminated

      • 阻塞等待一定时间:awaitTerminateion

  • 异步线程模式

    • 基本理解

      • 工作线程轮流移除处理无限多任务
      • 也可归类为分工模式,典型实现则是线程池,也是享元模式的体现
      • 原则:不同类型任务应该用不同线程池,可避免饥饿锁问题,进一步提升效率
      • 类比生活的中服务员与厨师,明显属于两类不同任务类型,应该分工协作效率更高
    • 饥饿问题(非死锁)

      • 思考生活案例:服务员与厨师
      • 若服务员和厨师两个人都能干点餐和炒菜的活
      • 如果客人较多,有可能会导致所有线程一直处于点餐,而没有人做菜
      • 解决方案:不同任务使用不同线程池
  • 线程池线程数量

    • 过小,容易导致饥饿问题
    • 过大会频繁上下文切换,效率低
    • CPU密集型运算,经验:CPU核数+1,+1是保证当前线程由于也缺失故障导致暂停,额外的线程能顶上去,保证CPU时钟周期不浪费
    • I/O密集型,如文件读写、RPC调用、数据库读写
    • IO密集型经验公式:线程数=核数*期望CPU利用率 * 总时间(CPU计算时间+等待时间)/CPU计算时间
    • 举例:4核CPU,计算时间占50%,其他等待50%,期望CPU被100%利用,则结果为8个线程比较合适
    • 时间占比一般可用监控工具观察,不需精确,取大致估计值即可
  • 任务调度线程池

    • 没有该线程池之前,可用Timer来实现

    • Timer特点

      • 背后就一个线程,任务不能并发执行
      • 前序任务耗时较长,将影响接续任务
      • 可靠性差,前序任务异常可能影响后续任务
      • 阿里巴巴规约已明显做提醒使用任务调度线程池代替
    • newScheduledThreadPool

      • 基本使用
      • 案例:每周四18:00执行任务,编写对应代码实现
/**
 * @author 钦尘
 * @date 2021/8/4 23:30
 * @description 线程池常用工具类
 */
@Slf4j(topic = "test.TestThreadPoolExecutors")
public class TestThreadPoolExecutors {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(() -> {
            log.info("执行任务1");
        });

        pool.execute(() -> {
            log.info("执行任务2");
        });

        pool.execute(() -> {
            log.info("执行任务3");
        });


        // Executors.newCachedThreadPool();
        // Executors.newSingleThreadExecutor();

        // submit 方法

        TestSubmit(pool);

        TestInvokeAll(pool);

        TestInvokeAny(pool);

        TestSchedulePool();
    }

    /**
     * 任务调度线程池
     */
    private static void TestSchedulePool() {
        /**
         * 多个任务之间异常,延时互不影响
         */
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
        scheduledPool.schedule(() -> {
            log.info("延迟1s, 执行task1");
        }, 1, TimeUnit.SECONDS);

        // 以某个速率周期性执行
        scheduledPool.scheduleAtFixedRate(() -> {
            log.info("定时每隔1s反复执行");
            // 如果执行任务耗时较长,超出间隔,则会在执行完成下,立即执行下次任务
            // 这种机制可防止任务重叠执行
            // Sleeper.sleep(2);
        }, 1, 1, TimeUnit.SECONDS);

        // 以某个速率周期性执行,注意与上面scheduleAtFixedRate的区别
        scheduledPool.scheduleWithFixedDelay(() -> {
            log.info("定时每隔1s反复执行");
            // 任务从上一次任务结束时间开始计算下个周期,这里则会间隔3秒执行
            Sleeper.sleep(2);
        }, 1, 1, TimeUnit.SECONDS);

        // 异常处理
        scheduledPool.schedule(() -> {
            log.info("异常信息模拟,默认不会抛出异常");
            int i = 1 / 0;
            // 以上异常不会抛出,方式1,可以自己手动捕获,方式2可以用submit,返回 Future,可以感知异常
        }, 1, TimeUnit.SECONDS);
    }

    private static void TestInvokeAny(ExecutorService pool) throws ExecutionException, InterruptedException {

        // 返回一个最先得到的结果
        String result = pool.invokeAny(Arrays.asList(
                () -> {
                    log.info("begin1");
                    Sleeper.sleep(1);
                    return "1";
                },
                () -> {
                    log.info("begin2");
                    Sleeper.sleep(2);
                    return "2";
                }, () -> {
                    log.info("begin3");
                    Sleeper.sleep(3);
                    return "3";
                })
        );

        log.info("invokeAny() 执行的任务结果为 {}", result);

    }

    private static void TestInvokeAll(ExecutorService pool) throws InterruptedException, ExecutionException {
        List<Future<Object>> futures = pool.invokeAll(Arrays.asList(
                () -> {
                    log.info("begin");
                    Sleeper.sleep(1);
                    return "1";
                },
                () -> {
                    log.info("begin");
                    Sleeper.sleep(2);
                    return "2";
                }, () -> {
                    log.info("begin");
                    Sleeper.sleep(3);
                    return "3";
                })
        );
        for (Future<Object> future : futures) {
            log.info("多个线程执行结果 {}", future.get());
        }
    }


    private static void TestSubmit(ExecutorService pool) throws InterruptedException, ExecutionException {
        Future<Object> future = pool.submit(() -> {
            log.info("执行任务");
            Sleeper.sleep(1);
            return "ok";
        });

        log.info("任务执行结果 {}", future.get());
    }
}
  • Tomcat线程池

    • 连接器即用到了线程池实现
    • 具体参看tomcat-embed-core源码
    • Tomcat配置项了解
  • Fork/Join

    • JDK1.7后加入,体现的是分而治之思想
    • 大任务拆小任务,分配给不同线程同时处理
    • 最后将各线程执行结果进行归并
    • 代码案例
/**
 * @author 钦尘
 * @date 2021/8/5 23:47
 * @description 应用fork-join,实现求 1 - n的和
 */
@Slf4j
public class TestForkJoin {

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        Integer result1 = pool.invoke(new MyTask1(5));
        log.info("计算结果1:{}", result1);

        Integer result2 = pool.invoke(new MyTask2(1, 5));
        log.info("计算结果2:{}", result2);
    }

}

/**
 * 1-n之间整数求和,思考下面的实现的高级之处
 * 存在的问题:任务之间相互等待依赖,并行度较低
 */
@Slf4j
class MyTask1 extends RecursiveTask<Integer> {

    private int n;

    public MyTask1(int n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if (n == 1) {
            return 1;
        }

        MyTask1 t1 = new MyTask1(n - 1);
        // 拆分,让一个线程去执行此任务
        t1.fork();

        log.info("fork {}, {}", n, t1);

        // 获取执行结果
        int result = n + t1.join();

        log.info("join {}, {}, {}", n, t1, result);
        return result;
    }
}

/**
 * 这样按范围拆分计算,可以提升线程并发度
 * 说明:任务拆分要多思考,怎么样合理,效率高,这方面其实对一般开发者有难度
 * 所以,到 JDK8 stream 底层自己自动实现了
 */
@Slf4j
class MyTask2 extends RecursiveTask<Integer> {

    private int begin;
    private int end;

    public MyTask2(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {

        if (begin == end) {
            return begin;
        }

        // 可以减少一次任务的拆分
        if (end - begin == 1) {
            return end + begin;
        }

        // 1 -> 5 = 3
        int mid = (end + begin) / 2;

        // 1,3
        MyTask2 t1 = new MyTask2(begin, mid);
        t1.fork();

        // 4,5
        MyTask2 t2 = new MyTask2(mid + 1, end);
        t2.fork();

        int result = t1.join() + t2.join();

        return result;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、锁、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少锁竞争、避免死锁等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值