深入理解Java多线程——线程池
程序员文章站
2022-04-15 17:36:48
线程池,除了池的功能外,还提供了更全面的线程管理、任务提交等方法。 ......
目录
定义
线程池,除了池的功能外,还提供了更全面的线程管理、任务提交等方法。
不同线程池的创建
juc的executors目前提供了5种不同的线程池创建配置
- newfixedthreadpooll(int nthreads)创建一个指定工作线程数量的线程池。其背后使用的是*的工作队列,每当提交一个任务就创建一个工作线程,如果工作线程数量达到活动队列数目,则将提交的任务存入到队列中等待。
- newcachedthreadpooll()创建一个可缓存的线程池,种用来处理大量短时间工作任务的线程池。这种类型的线程池特点是:
它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。工作线程的创建数量有限制为interger. max_value。
- 如果工作线程空闲超过1分钟,将自动终止并移出缓存。长时间闲置时,这种线程池,不会消耗什么资源。
其内部使用synchronousqueue作为工作队列
- newsinglethreadexecutor()创建一个单线程化的executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间只有一个线程在工作。
- newsinglethreadscheduledexecutor()和newscheduledthreadpool(int corepoolsize),创建的是个scheduledexecutorservice,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
- newworkstealingpool(int parallelism),java 8才加入这个创建方法,其内部会构建forkjoinpool,利用work-stealing算法,并行地处理任务,不保证处理顺序。
executor的组成
executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,使开发者不被太多线程创建、调度等不相关细节所打扰。
void execute(runnable command);
executorservice则更加完善,不仅提供service的管理功能,比如shutdown等方法,也提供了更加全面的提交任务机制,如返回future而不是void的submit方法。
<t> future<t> submit(callable<t> task);
- 工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为0的synchronousqueue(newcachedthreadpool),也可以是像固定大小线程池 (newfixedthreadpool),使用linkedblockingqueue。
private fnal blockingqueue<runnable> workqueue;
- 内部的“线程池”,指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。线程池的工作线程被抽象 为静态内部类worker,基于aqs实现。
private fnal hashset<worker> workers = new hashset<>();
- corepoolsize,所谓的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了allowcorethreadtimeout)。对于不同的线程池,这个值可能会有很大区别,比
如newfixedthreadpool会将其设置为nthreads,而对于newcachedthreadpool则是为0。 - maximumpoolsize,顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对于 newfixedthreadpool,当然就是nthreads,因为其要求是固定大小,而newcachedthreadpool则是integer.max_value 。
- keepalivetime和timeunit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。
public threadpoolexecutor(int corepoolsize, int maximumpoolsize, long keepalivetime, timeunit unit, blockingqueue<runnable> workqueue, threadfactory threadfactory, rejectedexecutionhandler handler)
- ctl变量是一个非常有意思的设计,它被赋予了双重角色,通过高低位的不同,既表示线程池状态,又表示工作线程数目,这是一个典型的高效优化。
private fnal atomicinteger ctl = new atomicinteger(ctlof(running, 0)); // 真正决定了工作线程数的理论上限 private static fnal int count_bits = integer.size - 3; private static fnal int count_mask = (1 << count_bits) - 1; // 线程池状态,存储在数字的高位 private static fnal int running = -1 << count_bits; … // packing and unpacking ctl private static int runstateof(int c) { return c & ~count_mask; } private static int workercountof(int c) { return c & count_mask; } private static int ctlof(int rs, int wc) { return rs | wc; }
线程池生命周期
execute()方法
public void execute(runnable command) { … int c = ctl.get(); // 检查工作线程数目,低于corepoolsize则添加worker if (workercountof(c) < corepoolsize) { if (addworker(command, true)) return; c = ctl.get(); } // isrunning就是检查线程池是否被shutdown // 工作队列可能是有界的,ofer是比较友好的入队方式 if (isrunning(c) && workqueue.ofer(command)) { int recheck = ctl.get(); // 再次进行防御性检查 if (! isrunning(recheck) && remove(command)) reject(command); else if (workercountof(recheck) == 0) addworker(null, false); } // 尝试添加一个worker,如果失败以为着已经饱和或者被shutdown了 else if (!addworker(command, false)) reject(command); }
线程池大小的设置
如果我们的任务主要是进行计算,通常建议按照cpu核的数目n或者n+1。
如果是需要较多等待的任务,例如i/o操作比较多,可以参考brain goetz推荐的计算方法:线程数 = cpu核数 × (1 + 平均等待时间/平均工作时间)
线程池的使用
要注意:
- 避免任务堆积。工作队列是*的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现oom。
- 避免过度扩展线程。通常在处理大量短时任务时,使用可缓存的线程池,但很难明确设置线程数目。
- 避免线程泄漏。往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放,当线程数目不断增长时造成溢出。
- 避免死锁
- 避免在使用线程池时操作threadlocal。因为threadlocalmap中废弃项目的回收依赖于显式地触发,否则就要等待线程结束,内存自动回收弱引用,进而回收相应threadlocalmap,但worker线程往往是不会退出的,这就容易出现oom。
参考
《java并发实战》
《java核心技术36讲》杨晓峰
上一篇: 乾隆的生母是谁?她的一生是如何渡过的?
下一篇: http -请求(get和post)