Java线程池

当我们需要在Java中处理大量的并发任务时,线程池是一种非常有用的工具。线程池可以帮助我们管理和复用线程,从而提高程序的性能和可伸缩性。在本篇技术博客中,我们将深入探讨Java线程池的概念、用法和最佳实践。

什么是线程池?

在Java中,线程池是一种基于池化思想管理线程的工具。线程池是通过java.util.concurrent包中的Executor框架来实现的。Executor框架提供了一个Executor接口和一个ThreadPoolExecutor类,可以用来创建和管理线程池。通过使用线程池,我们可以将任务提交给线程池,线程池会从池中选择一个可用的线程来执行任务。当线程执行完任务后,它将返回到池中,等待下一个任务的到来。线程池可以避免我们频繁的创建线程和销毁现场,带来不必要的性能损耗。

先来看看在没有使用线程池之前,我们是如何执行任务的:

public class Task implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

这是一个任务,任务的内容是输出当前线程名称,接下来我们执行该任务。

public class Demo19 {
    public static void main(String[] args) {
        Runnable task=new Task();//创建任务
        Thread thread=new Thread(task);//创建线程
        thread.start();
    }
}

首先,将任务创建出来,然后创建线程,接着将任务传递给线程,之后执行线程。我们发现程序输出Thread-0。我们发现一个线程只能执行一个任务,不能连续执行任务,例如,如果我们有三个任务,就只能再创建两个线程去执行另外两个任务。

public class Demo19 {
    public static void main(String[] args) {
        Runnable task1=new Task();//创建任务
        Runnable task2=new Task();//创建任务
        Runnable task3=new Task();//创建任务
        Thread thread1=new Thread(task1);//创建线程
        Thread thread2=new Thread(task2);
        Thread thread3=new Thread(task3);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这些线程执行完任务之后就销毁了,这也暴露了一个问题,线程不能复用,重复创建和销毁又耗时耗资源。,这就有了我们的线程池。

我们来看看线程池是怎么执行任务的:

public class Demo20 {
    public static void main(String[] args) {
        Runnable task1=new Task();//创建任务
        Runnable task2=new Task();//创建任务
        Runnable task3=new Task();//创建任务
        //创建只有一个线程的线程池
        ExecutorService threadPool= Executors.newSingleThreadExecutor();
        //提交任务
        threadPool.execute(task1);
        threadPool.execute(task2);
        threadPool.execute(task3);
        //关闭线程池
        threadPool.shutdown();
    }

}

我们创建了一个只有一个线程,这不是重点,重点是这个线程是可以复用的,调用execute方法,将需要执行的任务提交给线程池。任务提交以后,线程池会自动分配线程去执行提交过来的任务,最后调用它的shutdown方法关闭线程池。当线程池中的任务全部执行完毕一行,线程池则会关闭。

程序输出三个名称一样的线程,说明一个线程执行了三个任务,足以证明线程得到复用

对比线程,线程池的好处有很多,

Java线程池的优点包括:

  1. 提高性能:线程池可以重复利用已经创建的线程,避免了重复创建和销毁线程的开销,从而提高了应用程序的性能和响应速度。

  2. 提高可管理性:线程池可以限制并发线程的数量,避免了过多的线程竞争导致的资源浪费和性能下降。此外,线程池还可以提供监控和管理功能,例如线程池大小、线程池状态、线程执行时间等,方便开发人员进行调试和优化。

  3. 提高稳定性:线程池可以避免由于线程过多导致的系统崩溃和资源耗尽的问题,从而提高了应用程序的稳定性和可靠性。

  4. 提高可扩展性:线程池可以根据应用程序的需求动态调整线程池大小,从而提高了应用程序的可扩展性和灵活性。

总之,Java线程池是一种非常有用的多线程编程工具,可以提高应用程序的性能、可管理性、稳定性和可扩展性,同时也可以避免一些常见的多线程编程问题,例如线程过多、线程竞争和资源浪费等

如何创建线程池?

创建线程池的方法一共有8种,但万变不离其宗,这个宗就是原生创建线程池的方式。这种方式也是阿里极其推荐的一种。在《阿里巴巴java开发手册》的第七章第四小节种这样写道:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式去创建,其他的创建方式有资源耗尽的风险,后续章节在介绍其他创建方式时也会提到。

这是线程池核心UML类图。

从图中我们可以看到,能实例化的只有ThreadPoolExecutor 和ScheduledThreadPoolExecutor这两个,其中ThreadPoolExecutor就是线程池的核心类,它一共有四个构造方法,不过都大同小异

线程池核心参数 

最后一个构造方法参数最多,我们就从它开始。该方法一共有七个参数,这七个参数的含义如图所示: 

前四个参数结合在一起看比较好理解,这里我把他们都赋上值: 

线程存活时间和时间单位结合在一起就是:空闲线程的存活时间是10s ,10s内没工作就会被销毁。(如果空闲时间是0的话,空闲线程不会被销毁)

这就相当于我们构造了一个这样的线程池:

核心线程数是10,表示线程池中有10给核心线程,成为核心线程的好处就是,只要线程池不关闭,它就不会被销毁。最大线程数为25,表示线程池中最多允许有25个线程,核心线程之外的线程是非核心线程,非核心线程没有执行任务的话是要被清理的,在被清理之前能存活多久,取决于 线程存活时间和时间单位。

WorkQueue

任务队列,是放任务的容器,提交给线程池的任务都存放在这里,线程池的线程们也都是在这领取的任务。

我们一般使用的是LinkedBlockingQueue:链式阻塞队列,这是一个基于链表的阻塞队列,还有一个常用的是ArrayBlockingQueue:数组阻塞队列,除此以外,大家也可以尝试其他的阻塞队列。

threadFactory

线程工厂,顾名思义,可以指定线程如何生产,它是一个接口,实现里面的newThread方法,可以自定义线程的相关设置,例如我们可以指定线程名称,还可以指定线程是否为后台线程等等。

@Override
    public Thread newThread(Runnable r) {
        //创建线程,并指定任务;
        Thread thread=newThread(r);
        //设置线程名称
        thread.setName("线程1");
        //返回线程
        return thread;
    }

 如果不想自定义线程工厂,我们也可以使用Executors类中默认线程工厂,实际开发中我们用自定义的比较多,因为我们可以自定义名称,方便我们后续查看日志。

Handler

任务拒绝策略,在什么情况下,我们提交给线程池的任务会被拒绝呢?同时满足以下四种情况,我们提交给线程池的任务会被拒绝:

  1. 线程池中的线程已满
  2. 无法再继续扩容
  3. 没有空闲线程,所有线程都在执行任务
  4. 任务队列已满,无法再存入新任务

线程池拒绝我们任务的方式也有四种,如图所示:

接下来我们开始开始创建一个线程池:

先定义一个CustomThreadFactory类,实现ThreadFactory接口,重写newThread方法,定义一个AtomicInteger类型的计数器来保证线程安全,初始值为1,该计数器用在线程名称中给线程编号。创建线程实例,并指定任务。自定义线程名称,每创建一个线程计数器递增一次,最后返回线程实例。

public class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger i=new AtomicInteger(1);
    @Override
    public Thread newThread(Runnable r) {
        //创建线程并指定任务
        Thread thread=newThread(r);
        //设置线程名称
        thread.setName("线程"+i.getAndIncrement()+"号");
        //返回实例
        return thread;
    }
}

接下来编写一个任务,任务内容是输出当前线程名称,我们来执行该任务

public class Task implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

创建三个任务,然后创建一个线程池,核心线程数为10,最大线程数为25,空闲线程存活时间为10,时间单位为s,任务队列采用链式阻塞队列,线程工厂为刚刚自定义的线程工厂,任务拒绝策略采用默认。接下来向线程池中提交任务,最后关闭线程池。

Task task1=new Task();//创建任务
        Task task2=new Task();//创建任务
        Task task3=new Task();//创建任务
        //创建线程池
        ThreadPoolExecutor threadPool=new ThreadPoolExecutor(10,
                25,10L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),new CustomThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        //提交任务
        threadPool.execute(task1);
        threadPool.execute(task2);
        threadPool.execute(task3);
        //关闭线程池
        threadPool.shutdown();
    }

执行任务,从执行结果来看,我们自定义的线程工厂生效了,使用原生方式创建线程池成功。

 创建线程池的方式多种多样,但这三种千万别用在正式环境中,它们分别是:FixedThreadPool(固定大小的线程池),SingleThreadExecutor(单个线程的线程池),CachedThreadPool(可缓存的线程池)

这三种创建方式都在Executors工具类中,所有以new开头的方法都可以创建线程池。

一共有12个这样的方法,去掉重载方法后,也就剩下6个。看似方式繁多,其实搞懂它们一点都不难,因为它们都和ThreadPoolExecutors有直接或间接关系,ThreadPoolExecutor是创建线程池的原生方式。

 FixedThreadPool,SingleThreadExecutor,CachedThreadPool内部都采用ThreadPoolExecutor来创建线程池,只不过内部做了一些简化参数的工作。

接下来我们就依次来了解这三种创建线程池的方式:

FixedThreadPool内部采用ThreadPoolExecutor来创建线程池,并且最大线程数和核心线程数一样,意味着它里面全都是核心线程,空闲线程存活时间为0,这样空闲线程就不会被销毁,任务队列采用的是LinkedBlockingQueue,需要注意的是,此队列有资源耗尽的风险。因为LinkedBlockingQueue的容量为Integer的最大值(2^31),这么多任务内存随时都有可能爆掉。所以FixedThreadPool这种创建方式极力不推荐使用。

另外,在《阿里巴巴Java开发手册》的第一章第7小节第四条中这样写道:FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

在阿里内部,直接就明令禁止使用它们来创建线程池。

SingleThreadExecutor内部也采用ThreadPoolExecutor来创建线程池,并且最大线程数和核心线程数一样,意味着它里面全都是核心线程,空闲线程存活时间为0,这样空闲线程就不会被销毁,任务队列采用的是LinkedBlockingQueue,风险就不再赘述了

CachedThreadPool意为可缓存的线程池,它里面出来核心线程以外还有非核心线程,内部也采用ThreadPoolExecutor来创建线程池,核心线程数是0,意味着里面全部都是非核心线程,非核心线程空闲下来是要被销毁的,空闲线程存活时间为60s,最大线程数是Integer的最大值风险不言而喻。任务队列采用的是SychronousQueue,这是一个同步队列。

提交任务的方式

提交任务的方式有两组,分别是execute和submit

Execute

该方法位于executor接口中,作用是向线程池中提交Runnable任务,我们知道Runnable任务是一个无返回值的任务,所以execute知识和提交无返回值的任务,如果我们的任务是有返回值的那么我们就需要创建Callable任务,他是一个有返回值的任务,Callable任务执行完会将任务执行结果封装到Future对象中,然后返回给调用者,调用者再通过Future对象获取结果。

在execute方法中,提交的任务如果被拒绝,则抛出任务拒绝异常,提交的任务不能为null,否则会抛出空指针异常。

Submit

该方法位于ExecutorService接口中,一共有三个submit方法

它们的作用稍有不同,这里我将三个方法的作用分别列举出来,他们的返回值类型都是Future类型,而且都带泛型,任务执行结果就封装在Future对象里面。

 

Future是一个接口,该接口定义了方法与任务执行结果相关的功能 ,它里面有五种可用的方法,这里就不细说了。

回到submit本身,我们先来看它的第一种实现方法,它的作用是提交Runnable任务,返回一个Future对象。Runnable都没有返回值了,为什么还要返回Future对象你?这是因为Future除了可以获取任务执行结果以外还可以观察任务是否执行完毕,以及取消任务等等操作。所以Future对象可以选择接收,也可以选择不接收。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值