线程池一些知识解答

为什么需要线程池:线程创建、销毁

线程的建立和销毁,维护一个线程池处理多任务,更加有效利用cpu。那么主要是浪费那些资源呢?我们来分析创建一个线程的过程
上面已经提到了,创建一个线程还要调用操作系统内核API。为了更好的理解创建并启动一个线程的开销,我们需要看看 JVM 在背后帮我们做了哪些事情:

  • 它为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧
    每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
    一些支持本机方法的 jvm 也会分配一个本机堆栈
  • 每个线程获得一个程序计数器,告诉它当前处理器执行的指令是什么
  • 将与线程相关的描述符添加到JVM内部数据结构中
  • 线程共享堆和方法区域
  • 系统创建一个与Java线程对应的本机线程
    Java 中的线程模型是基于操作系统原生线程模型实现的,也就是说 Java 中的线程其实是基于内核线程实现的,线程的创建,析构与同步都需要进行系统调用,而系统调用需要在用户态与内核中来回切换,代价相对较高,线程的生命周期包括「线程创建时间」,「线程执行任务时间」,「线程销毁时间」,创建和销毁都需要导致系统调用。

这段描述稍稍有点抽象,用数据来说明创建一个线程(即便不干什么)需要多大空间呢?答案是大约 1M 左右。如果每个用户请求都新建线程的话,1024个线程就占用了1个G的内存,看来不对线程进行管理隐患很大,于是提出了线程池的概念。

线程池的使用

一般我们都会用Executors来创建线程,这是一个线程池工厂类,调用各类方法可以获得相应的线程池。线程池基本都是利用ThreadPoolExecutor类来创建的,类似Executors.newFixedThreadPool的内部实现代码:

return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());  # 队列大小是 Integer.MAX_VALUE

我们根据入参顺序来刨解:

  • corePoolSize:线程的数量
  • maximumPoolSize:最大线程数量
  • keepAliveTime:线程存活的时间数量
    这里要特殊说明,只有当打活跃的线程数大于corePoolSize,keepAliveTime才会起作用,线程存活的时间超过keepAliveTime就会回收线程,也就是将线程销毁。
  • unit :存活时间单位,是秒还是分钟还是小时
  • workQueue:任务队列,用来存储排队执行的任务

通过参数来理解线程池的大致流程:

  • 1、新来任务,当前线程数量小于corePoolSize,新建线程执行任务
  • 2、线程数量等于corePoolSize,把任务放到workQueue
  • 3、workQueue满了,线程数量小于maximumPoolSize,新建线程执行任务
  • 4、workQueue满了,线程数量等于maximumPoolSize,拒绝任务
  • 5、workQueue任务越来越少,线程不停的从里面拿任务执行。
  • 6、workQueue空了,有线程空闲并且线程数量大于corePoolSize,根据keepAliveTime来销毁多余的线程,一直到线程数量等于corePoolSize
  • 7、没有任务,当前活跃的线程数量 == corePoolSize ,不会被销毁

Excutors工厂类 创建线程池

  • newFixedThreadPool
    corePoolSize 等于 maximumPoolSize的线程池,固定的线程数大小。
    缺点:任务队列是LinkedBlockingQueue,最大任务数是 Integer的最大值,是个坑,积累的任务数非常多
  • newCachedThreadPool
    缺点:maximumPoolSize最大为 Integer.MAX_VALUE,容易造成堆外内存溢出,SynchronousQueue入队出队必须同时传递,因为这个线程池线程最大,也不需要存储队列任务
  • newSingleThreadExecutor
    单个线程的线程池,只有一个线程,处理慢一点而已,但确保线程是有序处理任务。
    缺点:LinkedBlockingQueue无边界的队列任务,处理太慢,都不会感知到。
  • newScheduledThreadPool
    支持定时及周期性的任务执行的线程池,maximumPoolSize的值是Integer.MAX_VALUE;DelayedWorkQueue是无界的,因此maximumPoolSize是无效的。处理延迟主要是靠DelayedWorkQueue。
    缺点:线程数量最大值太大,队列无界限。

阿里巴巴开发手册上:【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

因为它们或多或少的有一些问题,很容易在实际生产中引发事故,我们应该了解他们的缺点,分别来对应使用在不同的场景中,特别是自己手动创建线程池,我们应该了解这些参数该如何设置。

  • 无界队列 容易导致任务不停的追加,内存不被回收,可能 OOM
  • 线程池数目无限大,容易造成堆外内存溢出
  • 有界队列:ArrayBlockingQueue:基于数组的队列,创建时需要指定大小。

拒绝策略:

  • ThreadPoolExecutor.AbortPolicy (默认的执行策略)丢弃任务,并抛出 RejectedExecutionException 异常。
  • ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute 方法的线程执行该任务。
  • ThreadPoolExecutor.DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。
  • ThreadPoolExecutor.DiscardPolicy,丢弃任务,不过也不抛出异常。

如何保持线程复用

其实开启一个线程后,再线程的run 方法中 写一个循环,执行一个任务后就从队列中获取任务对象Worker,Worker对象是线程池定义的对象,一个Worker对象中都有一个线程,任务Worker的个数就表明线程的个数,不过代码里没有利用Worker集合的大小来做判断,而是利用了AtomicInteger对象来控制线程数量。同时为了保证避免线程的销毁,线程池稳定运行之后一般都会保证corePoolSize大小的线程是活跃的,不会主动销毁。

每一个任务 也都是一个Runnable对象

在线程池里每一个任务也都是一个可执行的Runnable对象,在线程的run 方法中执行 task.run()方法来执行对象,这样就执行了一个任务,要记住这里不是start方法,start方法是起一个线程来执行,想当于额外的起线程来执行代码,这样就不是线程复用,所以执行run方法执行线程的内容。

为何设计keepAliveTime 但又不销毁全部线程

主要是线程池要保证一定的线程活跃,所以不能全部销毁线程,避免频繁的创建和销毁,如果认为线程池线程一直不销毁占用系统字段,其实可以通过corePoolSize的大小来控制活跃的线程数量。

如何销毁线程

利用阻塞队列来实现,代码如下:

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ; 这里面keepAliveTime 是阻塞队列等待的时间,如果超过这个时间还没有任务,就返回null,这样work对象就执行结束了,线程就执行结束,自动就销毁掉了。调用关系:

runWorker()  - - >  getTask()

插入任务、获取任务

在超出核心线程数目后,就不再起线程,而是通过添加到阻塞队列里,暂缓任务,等有空闲线程再拉取任务。

  • 插入 :offer
  • 获取 :take poll 两个方法

参考博客

线程池位运算
手撕ThreadPoolExecutor线程池源码
为什么都说线程切换开销小于进程呢?
【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值