1. jdk提供的线程池有哪些,如何创建?
通过接口ExecutorService来表示线程池,在Java中,创建线程池通常使用java.util.concurrent.Executors工具类中的静态工厂方法。这些方法返回不同类型的ExecutorService,它们都是线程池的接口实现。以下是一些常用的创建线程池的方法:
- FixedThreadPool:创建一个固定大小的线程池。当所有线程都处于活动状态时,新任务将在一个队列中等待。
ExecutorService executor = Executors.newFixedThreadPool(int nThreads);
其中nThreads是线程池中的线程数。
- CachedThreadPool:创建一个可缓存的线程池,如果线程池的大小超过了处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService executor = Executors.newCachedThreadPool();
注意:由于可能创建大量线程,所以使用这种线程池时,要特别注意系统的资源限制。
- SingleThreadExecutor:创建一个单线程的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)一个接一个地执行。
ExecutorService executor = Executors.newSingleThreadExecutor();
- ScheduledThreadPool:创建一个定长的线程池,支持定时及周期性任务执行。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(int corePoolSize);
其中corePoolSize是线程池的基本大小。
- 自定义线程池:使用ThreadPoolExecutor类直接创建线程池,这个类提供了更多的配置选项,如线程工厂的设置、拒绝策略的选择等。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
其中:
corePoolSize:线程池基本大小
maximumPoolSize:线程池最大大小
keepAliveTime:线程池维护线程所允许的空闲时间
unit:时间单位
workQueue:用于存放待执行任务的队列
threadFactory:用于设置创建线程的工厂
handler:当线程池无法处理新任务时所使用的拒绝策略
常用的创建方式?
- ThreadPoolExecutor
- CacheThreadPoolExecutor 可缓存的 弹性大小
2. 实际开发我们该怎么使用?
实际开发中,线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题。
实际开发中,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。
FixedThreadPool 和 SingleThreadPool,允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool,允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以,一般不使用线程池的工具类来创建。
代码示例
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(
3, //核心线程数
5, //最大线程数,根据CPU密集型或者IO密集型来确定
1, //线程活跃时间
TimeUnit.SECONDS,//线程活跃时间单位
new ArrayBlockingQueue<>(3), //等待队列数量
Executors.defaultThreadFactory(), //线程工厂 来创建线程对象
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略 默认的抛异常
for (int i = 0; i < 8; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "---开始办理业务");
});
}
executorService.shutdown();
}
}
jdk自带的四种拒绝策略
任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。
第二种是当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
四种拒绝策略
- ThreadPoolExecutor.AbortPolicy 丢弃任务,并抛出 RejectedExecutionException异常。
- ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用execute方法的线程执行该任务。主线程,我们常用的拒绝策略。
- ThreadPoolExecutor.DiscardOldestPolicy : 抛弃队列最前面的任务,然后重新尝试执行任务。
T- hreadPoolExecutor.DiscardPolicy,丢弃任务,不过也不抛出异常。
线程池都有哪些状态?
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
线程池中 submit() 和 execute() 方法有什么区别?
- execute():只能执行 Runnable 类型的任务。
- submit():可以执行 Runnable 和 Callable类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
使用的堵塞队列?
一般使用堵塞队列的ArrayBlockingQueue。
FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。
队列从有界无界上分,常见的有界队列为ArrayBlockingQueue 和LinkedBlockingQueue 。
ArrayBlockingQueue基于数组实现的阻塞队列。
LinkedBlockingQueue 其实也是有界队列,但是不设置大小时就是无界的。
ArrayBlockingQueue 与 LinkedBlockingQueue 对比一哈ArrayBlockingQueue 实现简单,表现稳定,添加和删除使用同一个锁,通常性能不如后者LinkedBlockingQueue 添加和删除两把锁是分开的,所以竞争会小一些常见的无界队列。
ConcurrentLinkedQueue 无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常PriorityBlockingQueue 具有优先级的阻塞队列
DelayedQueue 延时队列,使用场景缓存:清掉缓存中超时的缓存数据。
线程池的大小如何设置?
区分程序是IO密集型或者计算为主的CPU密集型。
- 一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
- 如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。