概述
为什么需要线程池:线程创建、销毁
线程的建立和销毁,维护一个线程池处理多任务,更加有效利用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来创建线程池?会有什么问题呢?
最后
以上就是无奈手机为你收集整理的线程池一些知识解答的全部内容,希望文章能够帮你解决线程池一些知识解答所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复