java并发编程之线程池

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.MyThreadPool

什么是线程池?

    答:线程池就相当于是线程的管理者,他会帮我们去创建线程,回收线程。

使用线程池的好处是什么?

    答:使用线程池的好处有一下几点

        1.会提高效应效率,因为线程池中存在空闲线程,可直接进行执行,就省去了线程创建时间

        2.降低资源消耗,创建线程和销毁线程都会消耗系统资源,而线程池里存在空闲线程,而且线程也会复用,不会立即销毁。

        3.提高线程的可管理性,前面我们在 -- java并发编程之多线程基础 -- 中就说过了,线程是双刃剑,需要合理利用,如果线程    数超过一定数量,CPU会经不起消耗,这样反而会导致性能下降,所以我们需要合理创建线程。而线程池内部实现了队列,线程池将会多出来的任务存入到队列里面,那么这样就不会存在同时创建大量的线程了。而且线程池可以控制空闲的线程的存活时间,如果空闲线程在指定时间没用执行那么就会被销毁。如果想了解队列--------> java并发编程之队列

线程池的使用场景?

    答:在会频繁创建线程,销毁线程,以及大量并发的时候都可以使用我们的线程池进行管理线程。

怎么使用线程池?

首先我们看看下面的这两个类:

  • ThreadPoolExecutor:自定义线程池,可以自己根据需求创建线程池。
  • Executors:JDK已经实现好了的几种线程池方案,返回值是ExecutorService接口,ThreadPoolExecutor也实现该接口。

1:ThreadPoolExecutor

这就是ThreadPoolExecutor的几个构造函数,那么该构造函数有什么用呢?我们挑最多的来解释把。

ThreadPoolExecutor
名称 作用
corePoolSize 核心线程数,如果当前线程池中核心线程数<corePoolSize,那么有新的任务就会创建一个新的线程
maximumPoolSize 最大线程数,允许存在最大线程数量,如果超过该数量就执行拒绝策略
keepAliveTime 空闲回收时间,当线程池中的核心线程数大于corePoolSize的时候,超过这个时间,那么该线程会被终止
unit 时间单位,设置keepAliveTime的时间单位
workQueue 保存多出来的任务队列(阻塞,当任务数量超过corePoolSize就是保存到workQueue里面。
threadFactory 可以指定线程创建策略,创建线程的策略,大多数情况使用默认即可
handler 拒绝策略,当任务数量大于workQueue+maximumPoolSize的时候执行的决绝策略,大多数情况使用默认即可。

另外在介绍几个线程池常用的方法:

线程池常用方法
方法 作用
void shutdown(); 关闭线程池,就算有线程没用执行完毕,也会进行关闭线程
 boolean isShutdown(); 判断线程池是否关闭
<T> Future<T> submit(Callable<T> task); 执行任务的方法 ,这里只是举例,当然参数类型还可以是  Runnable 类型 ,该方法有返回值,可以抛出异常
void execute(Runnable command); 没用返回值的执行任务的方法,不可以抛出异常,只能是Runnable类型

好了对于线程池我们也基本理解了,接下来我们就是用代码更深入理解ThreadPoolExecutor:

/**
     * 第一个例子
     * 实现思路:使用CyclicBarrier模拟高并发情况。一次当线程数达到30才进行执行
     * 。但是我们的线程池设置的最大线程数和队列数量是不够30的 所以肯定会发生拒绝策略。
     */
    static final class MyThreadPoolExecutor01 extends Thread {

        //屏障 用于模拟多线程并发访问
        private CyclicBarrier cyclicBarrier;

        private ThreadPoolExecutor threadPoolExecutor;

        public MyThreadPoolExecutor01(CyclicBarrier cyclicBarrier,ThreadPoolExecutor threadPoolExecutor) {
            this.cyclicBarrier = cyclicBarrier;
            this.threadPoolExecutor = threadPoolExecutor;
        }

        @Override
        public void run() {
            System.out.println("我创建成功了-----------》"+Thread.currentThread().getName());
            try {
                //阻塞,只有30个线程就绪之后才会被放行
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            threadPoolExecutor.execute(new RunThread());
        }

        public static void main(String[] args) {
            //达到30和线程之后 统一放开
            CyclicBarrier cyclicBarrier = new CyclicBarrier(30);
            //创建一个 核心线程数为4 最大线程数为8,队列最大容量为8 自定义拒绝策略
            ThreadPoolExecutor threadPoolExecutor1 = new ThreadPoolExecutor(4,
                    8, 3L, TimeUnit.MINUTES,
                    new ArrayBlockingQueue<>(8),new MyHandler());
            //理论上 我们的这个线程池可以同时运行最大16个线程
            while (true){
                //为了防止跑得太快 我们阻塞一下生产的速度
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new MyThreadPoolExecutor01(cyclicBarrier,threadPoolExecutor1).start();
            }
        }

        /**
         * 这是线程池的拒绝策略,
         * 当任务数量大于 maximumPoolSize + workQueue 队列数量的时候的拒绝策略
         */
        static final class MyHandler implements RejectedExecutionHandler{

            //拒绝的时候发生的策略
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println(r.toString()+"被我们的线程池拒绝了");
            }
        }

        //该类是用来运行的线程
        static final class RunThread implements Runnable{
            @Override
            public void run() {
                System.out.println("我是线程---->"+Thread.currentThread().getName());
            }
        }
    }

运行结果:

        从上面的例子可以看到,当我们创建线程到30的时候,我们的线程池开始运行了,但是我们的线程只执行到了八,就执行了拒绝策略,这是为什么呢?因为我们最大线程数是设置的8,队列数量设置的也是8,也就是我们一共能接下16个任务,但是我们却使用CyclicBarrier让30个线程同时并行执行,那么16后面的线程肯定会被拒绝。

必读:        

        上面的例子,我们使用的是execute进行执行,这个方法是没用返回值的,如果想要线程执行有返回值,请使用submit进行执行任务,任务类就需要继承Callable<T>接口,T是返回类型,使用了submit之后会返回一个Future对象,使用Future对象的get方法就可以获取到返回值,但是这个返回值不是即时的,给你的值,并不代表就是真实的执行结果,因为我们都知道,多线程是在并行进行执行的,当在主线程中开一个线程进行执行的时候,马上就会执行下一步,如果这个时候我们的子线程还没有执行完毕,你就把结果拿着跑了,这样肯定是不合理的,另外get方法可以设置时间参数,如果指定时间没用运行完毕拿到真实执结果,那么就会抛出异常。下面的例子就是使用submit方式进行获取返回值,供参考。

@PostMapping(value = "/api/clothAward")
    public Object clothAward(@RequestBody JackpotParam jackpotParam){
        CountDownLatch countDownLatch = new CountDownLatch(1);
        try{
            Optional<MyOrder> myOrder = myOrderRepo.findById(jackpotParam.getMyOrderId());
            Assert.isTrue(myOrder.isPresent(),"该订单已经参与布奖,可以执行修改布奖操作");
            //使用线程池进行调用线程执行
            Future<Object> submit = threadPoolExecutor.submit(new JackPotCallable(jackpotParam, boxService, jackpotService, productService, false, getUserId()));
            //调用拿到结果的方法 
            Object object = getObject(countDownLatch, submit);
            //阻塞主线程,只有拿到真实结果才允许放行,不然会导致返回客户端结果不一致
            countDownLatch.await();
            return object;
        }catch (Exception e){
            countDownLatch.countDown();
            throw new DIYException("布奖失败:"+e.getMessage());
        }
    }
   
    
    //拿到执行结果的方法
    public Object getObject(CountDownLatch countDownLatch,Future<Object> submit){
        Object result = null;
        while (true){
            try {
                //25秒没用拿到执行结果,那么会抛出异常
                result = submit.get(25,TimeUnit.SECONDS);
                if(result != null){
                    countDownLatch.countDown();
                    break;
                }
            } catch (Exception e) {
                throw new DIYException("获取布奖结果超时:"+e.getMessage());
            }finally {
                countDownLatch.countDown();
            }
        }
        return result;
    }

        上面的例子就是拿到执行结果的方法,但是意义不大,因为这样就是个单线程了,主线程进行阻塞了,只是一个例子,举例说明怎么获取执行结果。

好了自定义线程池 ThreadPoolExecutor 就到这里了,有人说线程池,每次都要自己创建,那我忘记哪些参数怎么配置怎么办。

JDK为我们封装好了几个线程池,我们可以直接拿来使用即可。

2:Executors

该类的方法都是静态方法,可以直接进行调用

在介绍了ThreadPoolExecutor之后,这里我们就不过多使用代码演示,因为基本上使用方法都是一样的。我们介绍Executors的常用几种方法

        newCachedThreadPool一个最大线程数接近无限大的一个线程池,空余线程回收时间为1分钟,如果使用这个基本上就不会发生我们上面自定义实现的 ThreadPoolExecutor 出现拒绝策略,因为我们最大线程数才8,而这个线程池为 Integer.MAX_VALUE 这么大

        newFixedThreadPool:该线程池创建时要指定最大线程数,他的最大线程数和核心线程数都是一样的,并且如果有空余线程会立马被回收。但是他有一个LinkedBlockingQueue队列,基本上也算是一个无限大的一个线程池了。

        newScheduledThreadPool:核心线程数为指定的线程数,但是最大线程数也是Integer.MAX_VALUE,并且支持定时执行。

        newSingleThreadExecutor:改线程的核心线程数和最大线程数都是1,空余线程会被立马回收,但是队列是LinkedBlockingQueue。

这里我我们就使用一下 newScheduledThreadPool 可定时指定的线程池

/**
     * 例子二  Executors
     * 因为其他几个都是一样的 我们只演示 newScheduledThreadPool 定时执行
     */
    static final class MyThreadPoolExecutor02 extends Thread{

        public static void main(String[] args) {
            //该线程池 核心线程数 0 最大 基本上接近无限大 为Integer.MAX_VALUE 空闲回收时间为60秒
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
            while (true){
                //三秒执行一次 其余的任务会被缓存到 队列里面
                scheduledExecutorService.schedule(new RunThread(),3,TimeUnit.SECONDS);
            }
        }

        //该类是用来运行的线程
        static final class RunThread implements Runnable{
            @Override
            public void run() {
                System.out.println("我是线程---->"+Thread.currentThread().getName());
            }
        }
    }

至于这个演示的结果,我们就不展示了。他的意思就是,三秒会执行一次,然后其余的任务会保存到队列里面。

好了,到了这里我们的的线程池就讲解完毕了,如果有什么不对的地方请指出,谢谢大家阅读

另外线程不能胡乱创建,配置线程数量可分为两种:CPU密集型,和IO密集型号,这里就不过多解释。

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.MyThreadPool

发布了25 篇原创文章 · 获赞 9 · 访问量 3052

猜你喜欢

转载自blog.csdn.net/qq_40053836/article/details/100051996