美文网首页程序员
浅析-Java线程池

浅析-Java线程池

作者: 涉川gw | 来源:发表于2018-08-06 20:33 被阅读0次

    简介

    什么是线程池?顾名思义,线程池就是装线程的“池子”。如果把一个线程看做是一桶水,那么线程池就是用来装这一桶一桶水的池子,这个池子或大或小,Java中有四种类型的用来装线程的池子。为什么要用线程池这个东西呢?这大概跟为什么要修筑大坝蓄水一个道理吧,一方面节约内存资源,频繁创建销毁对象是会增加内存消耗的,一方面也可以在储备线程对象,当需要使用的时候直接从“池子”里抽出来,不就省去一次创建的过程了,别看这一点点内存,高并发下,算起来可以说是要命的。好了,下面我们一起来看看这几个类型的线程池分别是什么,有什么差异,分别适用在什么场景。

    • FixedThreadPool
      先看下怎么使用吧,show the code:
    ------------测试类---------------
    public static void main(String[] args){
            ExecutorService fixdPoll=Executors.newFixedThreadPool(3);
            fixdPoll.execute(new MyThread());
            fixdPoll.execute(new MyThread());
            fixdPoll.execute(new MyThread());
            singlePool.execute(new MyThread());
            fixdPoll.shutdowm(); 
        }
    ----------Thread 类---------
    public class MyThread extends Thread{
        @Override
        public void run() {
        System.out.println(TimeUtils.getNowTime()+"--->"+Thread.currentThread().getName()+" is running....");
            System.out.println(Thread.currentThread().getName()+" is running....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    -----------打印输出--------------
     03:40:46--->pool-2-thread-1 is running....
     03:40:46--->pool-2-thread-3 is running....
     03:40:46--->pool-2-thread-2 is running....
     03:40:48--->pool-2-thread-3 is running....
    

    根据打印输出的信息来看,其实也没多少东西。我们可以看到,newFixedThreadPool的时候有传一个参数3进去,这个3就是控制线程池最大活跃线程数的参数,这里46秒的时候正好有三个线程在工作,由于我们执行了四次execute,所以第四个需要等待有空位才能进去执行,从打印的信息可以看到48秒的时候,也就是经过了2秒钟,第四个线程开始执行,也就是说线程池有空位了,这是因为我在线程里sleep了2秒,也是就说一个线程工作2秒便结束,就会让出线程池的空位,给等待队列里的下一个排队者来执行。灵魂画手画的丐版流程图,不用看,hhh~。


    FixedThreadPool.png
    • SingleThreadExecutor
      一样的,先上代码吧:
    ----------测试类----------
    ExecutorService singlePool=Executors.newSingleThreadExecutor();
    singlePool.execute(new MyThread());
    singlePool.execute(new MyThread());
    singlePool.execute(new MyThread());
    singlePool.execute(new MyThread());
    singlePool.shutdown();
    ------------打印输出------------
    03:57:05--->pool-1-thread-1 is running....
    03:57:07--->pool-1-thread-1 is running....
    03:57:09--->pool-1-thread-1 is running....
    03:57:11--->pool-1-thread-1 is running....
    

    先上灵魂图吧


    SingleThreadExecutor.png

    从名字和打印的信息可以看出来,这个线程池每次都只有一个线程在工作,也就是过独木桥,每个新来的线程都得等线程池里的线程执行完才轮到下一个

    • CachedThreadPool
      先上代码:
    --------测试类--------
    ExecutorService cachePool=Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
               Thread.sleep(1000);
                cachePool.execute(new MyThread());
          }
    cachePool.shutdown();
    ---------打印输出-----------
    06:52:58--->pool-3-thread-1 is running....
    06:52:59--->pool-3-thread-2 is running....
    06:53:00--->pool-3-thread-3 is running....
    06:53:01--->pool-3-thread-2 is running....
    06:53:02--->pool-3-thread-1 is running....
    06:53:03--->pool-3-thread-2 is running....
    06:53:04--->pool-3-thread-1 is running....
    06:53:05--->pool-3-thread-2 is running....
    06:53:06--->pool-3-thread-1 is running....
    06:53:07--->pool-3-thread-3 is running....
    

    这个测试是让工作线程睡眠2秒,然后每隔1秒开启一个新的线程加入线程池,从结果可以看到,这个线程池只需开启三个线程,因为最早的线程1运行2秒后就结束了,而从第四个任务开始,线程1就是空闲的,此时线程池会复用线程1开执行新的加入的工作。这就是CachedThreadPool的工作特性,在有空闲线程的时候复用空的线程执行新的工作,没有空闲的就直接创建新的线程来加入线程池,当然,这些空闲下来的线程也不会永远留着,这样也会浪费资源的,所以,就会有一个存活时间,默认是60s,即当线程空闲60s还没有新的任务进来需要用到这个线程,就会将这个线程销毁。

    • ScheduledThreadPool
     ScheduledExecutorService exec=new ScheduledThreadPoolExecutor(1);
     exec.scheduleAtFixedRate(new Runnable() {
              @Override
              public void run() {
                  System.out.println("=======我是第一个任务========");
              }
          },1000,5000,TimeUnit.MILLISECONDS);
          exec.scheduleAtFixedRate(new Runnable() {
              @Override
              public void run() {
                  System.out.println("我是第二个任务--->"+TimeUtils.getNowTime());
              }
          }, 100, 2000, TimeUnit.MILLISECONDS);
      exec.scheduleWithFixedDelay(new Runnable() {
              @Override
              public void run() {
                  System.out.println("我是第三个任务--->"+TimeUtils.getNowTime());
              }
          },100,2000,TimeUnit.MILLISECONDS);
    ------------打印输出-----------
    我是第二个任务--->07:12:21
    我是第三个任务--->07:12:21
    =======我是第一个任务=====
    我是第二个任务--->07:12:23
    我是第三个任务--->07:12:23
    我是第二个任务--->07:12:25
    我是第三个任务--->07:12:25
    =======我是第一个任务=====
    我是第二个任务--->07:12:27
    我是第三个任务--->07:12:27
    我是第二个任务--->07:12:29
    

    定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的处理数据。第一个参数创建后到开始第一次执行的时间,第二个参数是第一次执行后隔多少秒再次执行,也就是循环时间间隔。
    scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
    schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

    • 深入父类ThreadPoolExecutor
      上面这么多个类型的线程池都是由这个ThreadPoolExecutor来创建的,下面我们就来浅浅地扒一下这个类的源码,以对线程池有更进一步的了解。
      最主要的是看看它的构造函数,源码中有如下注释和代码,英语能力强的可以自己看原文,顺手指出下我理解不对的地方是最好了。
        /**
         * Creates a new {@code ThreadPoolExecutor} with the given initial
         * parameters.
         *
         * @param corePoolSize the number of threads to keep in the pool, even
         *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
         * @param maximumPoolSize the maximum number of threads to allow in the
         *        pool
         * @param keepAliveTime when the number of threads is greater than
         *        the core, this is the maximum time that excess idle threads
         *        will wait for new tasks before terminating.
         * @param unit the time unit for the {@code keepAliveTime} argument
         * @param workQueue the queue to use for holding tasks before they are
         *        executed.  This queue will hold only the {@code Runnable}
         *        tasks submitted by the {@code execute} method.
         * @param threadFactory the factory to use when the executor
         *        creates a new thread
         * @param handler the handler to use when execution is blocked
         *        because the thread bounds and queue capacities are reached
         * @throws IllegalArgumentException if one of the following holds:<br>
         *         {@code corePoolSize < 0}<br>
         *         {@code keepAliveTime < 0}<br>
         *         {@code maximumPoolSize <= 0}<br>
         *         {@code maximumPoolSize < corePoolSize}
         * @throws NullPointerException if {@code workQueue}
         *         or {@code threadFactory} or {@code handler} is null
         */
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    
    

    corePoolSize:表示允许线程池中允许同时运行的最大线程数。线程池默认创建的时候是空的,当第一个任务进来的时候才会开始创建第一个线程,然后一直到线程数目到达最大线程数目,便停止继续创建新的线程,后来的任务只有进入等待队列等待空闲线程了,但是如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。具体怎么使用就需要根据具体业务需求来决定了。

    maximumPoolSize:线程池允许的最大线程数,当线程数量到达这个值之后,就不会继续往线程池加新的线程了,而是加入到等待队列。

    keepAliveTime:表示线程空闲是能存活等待下次复用的时间。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用。当到达存活时间后空闲线程就会被停止。

    unit :keepAliveTime 的单位,TimeUnit里有定义。

    workQueue:一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的核心线程数目的时候,线程会进入阻塞队列进行阻塞等待。这个队列一般分三种类型,1、有界任务队列ArrayBlockingQueue:基于数组的先进先出队列。2、无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE。3、直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

    threadFactory:线程工厂,用来创建线程。

    handler:表示当线程数达到了最大线程数但是又有新的任务到来时采取的拒绝策略。AbortPolicy:丢弃任务并抛出RejectedExecutionException

    CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

    DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

    DiscardPolicy:丢弃任务,不做任何处理。
    接下来研究下执行和停止这两个方法。

        public void execute(Runnable command) {
            if (command == null)
                throw new NullPointerException();
            /*
             * Proceed in 3 steps:
             *
             * 1. If fewer than corePoolSize threads are running, try to
             * start a new thread with the given command as its first
             * task.  The call to addWorker atomically checks runState and
             * workerCount, and so prevents false alarms that would add
             * threads when it shouldn't, by returning false.
             *
             * 2. If a task can be successfully queued, then we still need
             * to double-check whether we should have added a thread
             * (because existing ones died since last checking) or that
             * the pool shut down since entry into this method. So we
             * recheck state and if necessary roll back the enqueuing if
             * stopped, or start a new thread if there are none.
             *
             * 3. If we cannot queue task, then we try to add a new
             * thread.  If it fails, we know we are shut down or saturated
             * and so reject the task.
             */
            int c = ctl.get();
          //当前线程数小于核心线程,直接创建线程并启动任务
            if (workerCountOf(c) < corePoolSize) {
                if (addWorker(command, true))
                    return;
                c = ctl.get();
            }
          //线程数大于核心线程数,尝试进去等待队列
            if (isRunning(c) && workQueue.offer(command)) {
                int recheck = ctl.get();
                if (! isRunning(recheck) && remove(command))
                    reject(command);
            // 如果当前worker数量为0,通过addWorker(null, false)创建一个线程,其任务为null
                else if (workerCountOf(recheck) == 0)
                    addWorker(null, false);
            }
            // 3.尝试将线程池的数量扩充至小于maxPoolSize,如果到达最大线程数,则拒绝任务
            else if (!addWorker(command, false))
                reject(command);
        }
    

    这是执行任务的方法,其中可以看出当任务线程为null的时候,就抛出异常,然后如果池的大小大于核心池的大小,新进的任务就会被放入等待队列。当线程数达到了最大线程,就会采取拒绝策略拒绝新进任务。

        /**
         * Initiates an orderly shutdown in which previously submitted
         * tasks are executed, but no new tasks will be accepted.
         * Invocation has no additional effect if already shut down.
         *
         * <p>This method does not wait for previously submitted tasks to
         * complete execution.  Use {@link #awaitTermination awaitTermination}
         * to do that.
         *
         * @throws SecurityException {@inheritDoc}
         */
        public void shutdown() {
            final ReentrantLock mainLock = this.mainLock;//获取可重入锁并上锁
            mainLock.lock();
            try {
                checkShutdownAccess(); //获取系统安全管理器,并检查权限
                advanceRunState(SHUTDOWN);
                interruptIdleWorkers();
                onShutdown(); // hook for ScheduledThreadPoolExecutor
            } finally {
                mainLock.unlock();
            }
            tryTerminate();
        }
    

    可以看出结束方法在真是调用结束前会调用的是系统安全接口,在系统层面对线程池进行保护,防止其发生意外。比如中断系统进程等,获取了安全管理器之后接下来再对其进行权限检查,并加锁控制。接下来就是取状态进行判断了,接着遍历循环整个线程池里的工作线程,一旦发现空闲的就进行打断终止。然后是调用tryTerminate方法来进行最后的关闭。

    总结

    本人水平有限,本文是在参考前人总结的基础上再加上点自己的理解写完的,感谢前人的总结。也希望有心人指点其中理解不对的地方。

    相关文章

      网友评论

        本文标题:浅析-Java线程池

        本文链接:https://www.haomeiwen.com/subject/wjmmvftx.html