全网最全Java线程池讲解!!!

本文介绍了Java线程池的概念,如何使用Executor框架创建不同类型的线程池,以及其在任务处理中的优势、工作原理和常见问题,包括线程复用、线程池大小设定和正确关闭。
摘要由CSDN通过智能技术生成

线程池

什么是线程池

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。

从JDK1.5开始,Java API提供了Executor框架可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)

线程池的基本使用

public class Main {

    public static void main(String[] args) {
        //获取线程池,newxxx则获取不同的线程池服务,例如newSingleThreadExecutor获取单线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();
        //向线程池提交工作任务,Runnable接口或者Callable的接口的实现,一般Callable接口使用submit,Runnable接口使用execute
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!");
            }
        });
        //关闭连接池服务
        executor.shutdownNow();
    }
}

启动多个工作任务

public class Main {
    public static void main(String[] args) {
        //获取线程池,newxxx则获取不同的线程池服务,例如newSingleThreadExecutor获取单线程池
//        ExecutorService executor = Executors.newSingleThreadExecutor();
         ExecutorService executor = Executors.newCachedThreadPool();  //可缓存的线程池
        //向线程池提交工作任务,Runnable接口或者Callable的接口的实现,一般Callable接口使用submit,Runnable接口使用execute
        for (int i = 0; i < 10; i++) {
            final int k = i;//如果在匿名内部类种使用外部的临时变量,则外部临时变量必须为final,只是final关键字可写可不写
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello World!--" + k+Thread.currentThread());
                }
            });
        }
        //关闭连接池服务
//        executor.shutdownNow();  当主线程执行到shutdownNow时不管线程工作是否执行完毕,立即关闭线程池
        executor.shutdown();  //当工作线程全部执行完毕自动关闭
    }
}

线程池的好处

  • 重用存在的线程,减少对象创建、消亡的开销,性能佳
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

线程池的工作原理

1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。

2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步

3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

线程池的实现源码

  • 可缓存线程池
public static ExecutorService newCachedThreadPool() {  
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 单线程池
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

线程池的具体实现类为ThreadPoolExecutor

public ThreadPoolExecutor(
		//核心线程数目
		int corePoolSize,
		//最大线程数[=核心线程数+工作线程数]
		int maximumPoolSize,
		//空闲线程的存活时间
		long keepAliveTime, 
		//时间单位【枚举类型】
		TimeUnit unit, 
		//workQueue所使用的拥塞队列
		BlockingQueue<Runnable>, 
		//线程工厂
		ThreadFactory threadFactory,
		//饱和拒绝策略
		RejectedExecutionHandler handler) {

1.corePoolSize(线程池中的核心线程数)

当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。

2.maximumPoolSize(线程池中允许的最大线程数)

如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。

3.keepAliveTime(线程空闲时的存活时间)

默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到线程池中的线程数为0。

4.unit(时间单位)

keepAliveTime参数的时间单位。

5.workQueue(任务缓存队列)

用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的任务就会被保存到任务缓存队列中,等待被执行。

一般来说,这里的BlockingQueue有以下三种选择:

  • SynchronousQueue:
    一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速度),可能出现无限制的线程增长。
  • LinkedBlockingQueue:
    基于链表结构的阻塞队列,如果不设置初始化容量,其容量Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,
    且始终没有空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。
  • ArrayBlockingQueue:
    基于数组结构的有界阻塞队列,按FIFO排序任务。

6.threadFactory(线程工厂)

创建新线程时使用的线程工厂。

7.handler(任务拒绝策略)

当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,就会采取任务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:

  • AbortPolicy(默认策略):
    丢弃任务并抛出RejectedExecutionException异常;
  • CallerRunsPolicy(返回调用者的线程执行):
    由调用execute方法的线程执行该任务;
  • DiscardPolicy(丢弃未进入队列的任务):
    丢弃任务,但是不抛出异常;
  • DiscardOldestPolicy:
    丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。

当然也可以根据应用场景实现RejectedExecutionHandler接口自定义饱和策略,如记录日志或持久化存储不能处理的任务。

Executors创建线程池

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程

    • 用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务
public static ExecutorService newCachedThreadPool() {
	//上限值太大
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
	60L, TimeUnit.SECONDS,
	new SynchronousQueue<Runnable>());
}
    
  • newFixedThreadPool 创建一个固定大小的定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

    • 因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制
 public static ExecutorService newFixedThreadPool(int nThreads) {
 			return new ThreadPoolExecutor(nThreads, nThreads,
			L, TimeUnit.MILLISECONDS,
			//无上限的链表结构
			new LinkedBlockingQueue<Runnable>());
 }
  • newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行

    • 可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景

    • public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService
      
  • newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

    • 适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景

      public static ExecutorService newSingleThreadExecutor() {
      	       return new FinalizableDelegatedExecutorService
                 (new ThreadPoolExecutor(1, 1,
                                          0L, TimeUnit.MILLISECONDS,
                      new LinkedBlockingQueue<Runnable>()【无上限的链表结构】));
          }
      
      
  • newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数

    • 创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

按照ali的规范不建议使用通过Executors创建的由JDK提供的线程池,如果需要线程池则建议自定义线程池参数new ThreadPoolExecutor

1、 线程池默认使用无界队列,任务过多导致OOM

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    executor.execute(() -> {
    	Thread.sleep(10000);
    });
}

使用newFixedThreadPool创建的线程池,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。

这是因为newFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM。

因此,工作中建议大家自定义线程池,并使用指定长度的阻塞队列。

2、线程池创建线程过多,导致OOM

newCachedThreadPool的构造函数:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

它的最大线程数是Integer.MAX_VALUE,如果创建了大量的线程也有可能引发OOM!

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

3、共享线程池,次要逻辑拖垮主要逻辑

不能将所有的业务都共享一个线程池,因为这样做,风险太高了,应当做线程池隔离!

4、线程池拒绝策略的坑,使用不当导致阻塞

线程池主要有四种拒绝策略

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

如果线程池拒绝策略设置不合理,就容易出现问题。可以尝试把拒绝策略设置为DiscardPolicy或DiscardOldestPolicy并且在被拒绝的任务,Future对象调用get()方法,那么调用线程会一直被阻塞。

日常开发中建议使用 Future.get() 时,尽量使用带超时时间的,因为它是阻塞的。f3.get(1,TimeUnit.SECONDS)

关闭线程池

如果线程池使用完,忘记关闭的话,有可能会导致内存泄露问题。所以使用完线程池后,记得关闭一下。同时,线程池最好也设计成单例模式,给它一个好的命名,以方便排查问题。

shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表

shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值