多线程(一) 线程池的原理和构造

一、线程池概述

线程经常用来同时处理一个程序的多个任务,但是在并发任务非常多并且处理时间短的情况下,使用线程就需要面临一个问题,假设我们把线程创建的时间看做T1,线程执行任务的时间看做T2,线程销毁的时间看做T3,学过小学数学的都知道,只有当T2的时间足够大时,这个线程才能执行更多的任务,而不是把时间花费在创建和销毁上。

然而实际开发中,很少能人为控制T2的时间,因此,如何缩减T1和T3的时间,就是目前的解决思路,也就是线程池对线程资源开销的解决思路。

线程池将一个或者多个线程多次反复利用,同时需要对并发任务进行处理(也就是在任务增加的情况下,动态的创建新的线程),同时也需要考虑在程序空闲的情况下,对资源的释放(也就是在没有新的任务的情况下,销毁已有的线程,释放占用的资源)。

 

二、线程池构造

通常我们通过Java提供的ThreadPoolExecutor类来配置一个线程池对象。

这个类重要的构造参数主要有以下几个:

1)corePoolSize:核心线程数

该参数决定线程池能够一直活跃的线程数量。每当一个任务交给线程池,只要当前核心线程数小于corePoolSize,那么就会新建一个线程运行新来的任务。

2)BlockingQueue<Runnable> workQueue:等待队列

队列一般分为有界队列和无界队列(无界队列因为会无限扩容的原因,一旦处理速度跟不上任务产生速度,任务堆积,就会导致内存膨胀溢出)。

核心线程数已经达到了设定大小时,新来的任务会交给等待队列。

3)maximumPoolSize:最大线程数

等待队列也满了的时候,线程池会进入996加班模式,也就是会继续new Thread来处理新来的任务(如果设置的maximumPoolSize==corePoolSize,那么这个线程池不会创建新的线程。)

4)RejectedExecutionHandler handler:拒绝执行策略

当一个任务被拒绝执行时(比如线程池和等待队列都爆满的情况下),会调用handler的rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法(如果有的话)。

5)keepAliveTime:保持活跃时间

这个参数主要用来限制非核心线程的存活时间。 确保不会有线程长时间进入空闲状态,占用资源。

但是该参数对核心线程无效,核心线程一旦被创建,除非人为关闭,否则一直存活。

6)TimeUnit timeUnit:keepAliveTime的单位

该参数是枚举类型,用来指定keepAliveTime的时间单位(秒,毫秒……),可以赋值TimeUnit.SECONDS表示秒。

7)ThreadFactory threadFactory:线程工厂

顾名思义,该对象主要用来创建线程对象。(JDK有提供默认的线程创建模式。)

 

三、线程池的创建

的基础之上,我们就可以开始构造适用于自己的线程池了。

为了演示线程创建和拒绝策略的执行,其中ThreadFactory RejectedExecutionHandler的实现需要自己手写一下。

在这里我先写好一个任务,继承了Runable接口

//因为是内部类所以加了static,如果不写成内部类,就不需要写
static class MyTask implements Runnable {
        private String taskName;

        public MyTask(String taskName) {
            this.taskName = taskName;
            System.out.println(getTaskName() + "is create");
        }

        @Override
        public void run() {
            System.out.println(getTaskName() + "is running");
            try {
                Thread.sleep(2500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getTaskName() + "is over");
        }

        public String getTaskName() {
            return "任务"+taskName;
        }

        @Override
        public String toString() {
            return "任务"+taskName;
        }
    }

接着实现一下线程池中ThreadFactory的创建

static class MyThreadFactory implements ThreadFactory {
        private int threadNo = 0;

        @Override
        public Thread newThread(Runnable r) {
            //线程池会调用该方法来创建线程对象
            ++threadNo;
            System.out.println("Thread" + threadNo + "is created");
            return new Thread(r,String.valueOf(threadNo));
        }
    }

接着准备一下拒绝执行策略

static class MyRejectedHandler implements RejectedExecutionHandler {

        //线程池在拒绝一个任务的请求之后,会调用该方法
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            
            MyTask mt = (MyTask) r;
            //获取此时的队列
            BlockingQueue<Runnable> workQueue = executor.getQueue();
            System.err.print(mt.getTaskName() + " is rejected,原因:");
            System.err.print("等待队列当前大小:"+workQueue.size() + "已经达到最大值,");
            //获取迭代器循环遍历所有的任务,取出任务名并且打印
            Iterator<Runnable> iterator = workQueue.iterator();
            String message = "";
            while(iterator.hasNext()){
                message += iterator.next().toString()+" ";
            }
            System.err.println("等待队列的任务是"+message);
        }
    }

现在万事俱备,可以创建线程池了

public static void main(String[] args) {
        //核心线程数
        int corePoolSize = 2;

        //最大线程数
        int maximumPoolSize = 4;

        //保持生命时间:为了节约资源,当一个线程空闲时间超过keepAliveTime时,该线程会被注销(默认对核心线程无效)
        long keepAliveTime = 2;

        //keepAliveTime的时间单位,属于枚举类型
        TimeUnit timeUnit = TimeUnit.SECONDS;

        //等待队列:构造函数表示队列的大小,当所有的核心线程都在忙碌时,新来的任务会被存储在此
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);

        //线程创建工厂:工厂设计模式,用来构建线程对象
        ThreadFactory threadFactory = new MyThreadFactory();

        //拒绝执行后处理器:当等待队列已满,新来的任务会被拒绝执行,此时会调用处理器的rejectedExecution方法来处理异常
        RejectedExecutionHandler handler = new MyRejectedHandler();

        //创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, handler);

        //创建核心线程
        int coreNo = threadPoolExecutor.prestartAllCoreThreads();
        System.out.println(coreNo+"个核心线程被创建");

        //循环创建任务
        for (int i=1;i<=10;i++){
            threadPoolExecutor.execute(new MyTask(String.valueOf(i)));
        }

        //程序执行之后不会自己停止,因为线程池还挂着呢。需要手动释放线程才能结束程序。
        


    }

最后看一下运行结果吧。

可以看到,一开始手动创建了所有的核心线程(空闲状态),之后开始循环创建任务。

接着任务1和任务2因为此时核心线程空闲,直接运行,任务3和任务4则在等待队列中等待。

之后又创建了任务5,此时核心线程和等待队列已经满了,但是线程数还没有达到最大,因此创建了新的线程3来执行这个新任务。

任务6和任务5的待遇相同。因为线程的创建需要时间,因此任务的运行和创建之间等待的时间较长。

任务7,8,9,10因为此时的线程数已经达到最大,等待队列也爆满,因此,全部都被拒绝执行并且执行了拒绝策略。

接着,任务1和任务2执行完毕,等待队列的任务3和任务4继续执行。

最后所有的任务都执行完毕,程序……还没停。

需要手动销毁线程池之后,程序才会正常关闭

 

 

四、线程池的关闭

一般情况下,线程池都是为了解决并发项目的任务而开启的,所以并不需要关闭线程池。但是如果想要关闭线程池的话,由于核心线程并不会自己销毁,因此需要手动结束线程,线程池想要自动销毁的话,需要满足两个条件。

  1. 线程池引用不可达。
  2. 线程池的线程数为0。

推荐通过调用线程池提供的方法来关闭线程。

主要有以下几个方法:

  • shutdown():该方法会准备关闭线程池,执行此方法,会等待所有任务结束之后,再关闭线程池。
  • shutdownNow():该方法会立刻关闭线程池,执行此方法,正在执行的任务会被强制中断。
  • getActiveCount():该方法会返回当前正在工作的线程数,当所有的线程都处于空闲状态时,返回值为0。

当线程池被关闭之后,线程池将无法再执行新的任务,否则会抛出异常!(准确来说,会执行拒绝执行策略代码)。

 

五、线程关闭机制

虽然在之前提到,使用shutdownNow()可以立即关闭线程池,但是其实质上并不是如此。

追踪源码我们可以发现,线程池的shutdownNow()方法,本质上还是调用了线程本身的关闭方法interrupt()。所以,shutdownNow()方法也不能保证立即关闭线程池。

void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    //shutdownNow()的底层实现是调用了线程对象的interrupt方法
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }

因为线程的关闭方法interrupt() 其本质上并没有关闭线程,能够直接关闭线程的stop()等方法都因为线程安全问题而废弃。interrupt()方法仅仅只是更改了线程对象的一个标志位,告诉这个线程,应该要关闭了,但是实际上是否关闭,取决于线程本身。因此,即便使用showdownNow()方法之后,线程池中的线程关闭还是不关闭,也要取决于线程自身。。。

这里根据应用场景有多个不同解决方案,如果任务是经常做循环或者递归操作,那么可以用这样的方式:

        @Override
        public void run() {
            for (int i=0;i<1000000;i++){
                //在方法体中添加校验关闭标志位,判断线程是否应该关闭
                if(Thread.interrupted()){
                    System.out.println("退出线程"+i);
                    break;
                }else{
                    i++;
                }
            }
        }

这个程序在使用线程池运行,并且使用shutdownNow()关闭后,结果如下

可以发现,该任务的循环并没有结束,线程就被关闭了。

如果去掉校验,改成这样:

        @Override
        public void run() {
            int i = 0;
            for (;i<1000000;i++){
                //在方法体中去掉标志位
                    i++;
            }
             System.out.println("退出线程"+i);
        }

运行结果:

即便使用了shutdownNow(),线程依旧做完了循环才关闭,这样看来在多次循环中如果不对标志位认为判断,shutdownNow()和shutdown()几乎没什么区别了。

 

 

 

参考原文地址:https://www.jianshu.com/p/f030aa5d7a28

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值