我是靠谱客的博主 无奈手机,最近开发中收集的这篇文章主要介绍线程池一些知识解答,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

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

线程的建立和销毁,维护一个线程池处理多任务,更加有效利用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来创建线程池?会有什么问题呢?

最后

以上就是无奈手机为你收集整理的线程池一些知识解答的全部内容,希望文章能够帮你解决线程池一些知识解答所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部