Bootstrap

带你了解线程池的创建和使用

1. 什么是线程池

线程池是一种并发编程的技术,用于管理和复用线程。在程序运行过程中,创建和销毁线程是一项昂贵的操作,而线程池的主要目的是通过预先创建一组线程,并在需要时重复利用它们,来减少线程创建和销毁的开销,从而提高程序的性能和响应速度。

2. 为什么使用线程池

一个线程大约占用的内存1M.

  1. 解决频繁创建线程和销毁线程消耗的性能。
  2. 解决大量创建线程而导致的内存泄露问题。

3. 如何创建线程池

java中提供了两种方式来创建线程池:

第一种: 通过工具类完成线程池的创建.[Executors]. 语法简单。但是阿里巴巴不建议使用。

第二种: 通过线程池类: ThreadPoolExecutor类. 语法复杂,但是阿里巴巴建议使用。因为它比较灵活。

线程的根接口: Executor. 里面只有一个方法: execute

子接口: ExecutorService。

3.1 使用工具类创建线程池

使用工具类创建线程池有四种方式,分别是:

  1. 固定大小的线程池对象newFixedThreadPool
  2. 单一线程池: newSingleThreadExecutor
  3. 可变线程池: newCachedThreadPool
  4. 延迟线程池: newScheduledThreadPool
3.1.1 固定大小的线程池

​ 固定大小的线程池是一种特定类型的线程池,其特点是在初始化时就确定了固定数量的线程,并且一直保持这个数量不变。

public class Thread01 {
    public static void main(String[] args) {
        // 固定大小的线程池对象
        // 里面的5为线程数量
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executorService.execute(
                ()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
        }
        
        // 关闭线程池
        executorService.shutdown();
    }
}

运行结果:

在这里插入图片描述

通过运行结果可以看出,线程只有1~5,5个线程,不会超出给定的线程数量。

3.1.2 单一线程池

​ 单一线程池是一种特殊的线程池,它只包含一个单独的工作线程。这意味着所有提交给单一线程池的任务都会由同一个线程顺序执行,这个线程会按照任务提交的顺序依次执行它们。

public static void main(String[] args) {
    // 单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    
    for (int i = 0; i < 10; i++) {
        executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
    }
    
    // 关闭线程池
    executorService.shutdown();
}

运行结果:

在这里插入图片描述

通过运行结果可以看出,从始至终都只有一个线程执行程序,因为单一线程池中只有一个线程。

3.1.3 可变线程池

​ 可变线程池是一种具有动态调整线程数量的线程池,其大小可以根据需要自动调整。

public static void main(String[] args) {
    // 可变线程池
    ExecutorService executorService = Executors.newCachedThreadPool();
    
    for (int i = 0; i < 11; i++) {
        executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
    }
    
    // 关闭线程池
    executorService.shutdown();
}

运行结果:

在这里插入图片描述

根据运行结果可以看出,打印11次,达到了十个线程,线程3打印了两次,这是因为打印第11次的时候,线程3执行完毕进入空闲状态,并且抢到了第十一次打印的线程。

如果我们把打印的次数换成50次

for (int i = 0; i < 50; i++) {
    executorService.execute(()-> System.out.println(Thread.currentThread().getName()+"~~~~~~~~"));
}

运行结果:

在这里插入图片描述

根据运行结果可以看出,此时达到了16个线程,根据两次对比可以看出,可变线程池的线程数量是根据需要自行调整的。

3.1.4 延迟线程池

​ 延迟线程池是一种专门用于处理需要延迟执行的任务的线程池。在这种线程池中,任务可以设定一个延迟时间,在指定的延迟时间之后才会被执行。延迟线程池通常用于处理需要在未来某个时间点执行的任务,比如定时任务或者某些需要等待特定条件满足后再执行的任务。

public static void main(String[] args) {
    // 延迟线程池
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);// 线程数
    for (int i = 0; i < 10; i++) {
        scheduledExecutorService.schedule(()-> System.out.println(Thread.currentThread().getName()+"~~~"),1, TimeUnit.SECONDS);
    }

    for (int i = 0; i < 10; i++) {
        scheduledExecutorService.scheduleAtFixedRate(()-> System.out.println(Thread.currentThread().getName()+"~~~"),1,2, TimeUnit.SECONDS);
    }
    
    // 关闭线程池
    scheduledExecutorService.shutdown();
}

​ 在这个代码中,第一个for循环中的schedule方法中有三个参数,第二个for循环中的scheduleAtFixedRate方法中有四个参数。

​ schedule方法中,第一个参数为要执行的任务,第二个参数为初始延迟时间,第三个参数为时间单位。即这个方法在1秒之后执行,会一次性把十次打印都打印出来,只是总体任务时间延迟。

​ scheduleAtFixedRate方法中,第一个参数为要执行的任务,第二个参数为初始延迟时间,第三个参数为延迟周期,第四个参数为时间单位。即这个方法在1秒之后执行,每过2秒打印一次,打印十次后结束。

3.2 通过线程池类创建线程池

在Java中,可以通过 ExecutorService 接口的实现类来创建线程池,这些实现类通常是 ThreadPoolExecutor 或者 ScheduledThreadPoolExecutor 的实例化对象。这两个类分别实现了基本的线程池和定时任务线程池。

下面通过ThreadPoolExecutor实现类来创建基本的线程池:

public static void main(String[] args) {
    
    // 创建堵塞队列,最大为5个
    BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue(5);
    ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(2,6,10, TimeUnit.SECONDS,workQueue);
    
    for (int i = 0; i <11; i++) {
        poolExecutor.submit(()->{
            System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~~");
        });
    }
    // 关闭线程池
    poolExecutor.shutdown();
}

在这个代码中,new ThreadPoolExecutor里参数的含义为:

  1. int corePoolSize,核心线程数的个数 2
  2. int maximumPoolSize,最大线程数量 5
  3. long keepAliveTime, 非核心线程允许空闲的时间 10
  4. TimeUnit unit, 时间的单位 秒,TimeUnit.SECONDS,workQueue
  5. BlockingQueue workQueue 堵塞队列 5

运行结果:

在这里插入图片描述

根据运行结果可以看出:最大线程数为6。

我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数:

a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);

在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);

紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;

此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;

假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;

最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班。(销毁线程)但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);

执行流程如下图所示:

在这里插入图片描述

接下来我们用一个案例来进一步理解这个过程:

综合案例-秒杀商品
案例介绍:
假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;
要求:
1:使用线程池创建线程
2:解决线程安全问题
思路提示:
1:既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
2:当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
3:使用synchronized控制线程安全,防止出现错误数据;
代码步骤:
1:编写任务类,主要是送出手机给秒杀成功的客户;
2:编写主程序类,创建20个任务(模拟20个客户);
3:创建线程池对象并接收20个任务,开始执行任务;
主程序类,测试任务类

实体类:

public class Phone implements Runnable{
    private static int number = 10;
    private String name;

    public Phone() {
    }

    public Phone(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // 双重锁
        if (number > 0){
            synchronized (Phone.class){
                if (number > 0){
                    System.out.println(name+"秒杀成功,抢到了"+(11-number)+"号手机");
                    number--;
                    return;
                }
            }
        }
        System.out.println(name+"秒杀失败");
        
    }
}

测试类:

public class Test {
    public static void main(String[] args){
        // 创建堵塞队列,最大为5个
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, workQueue);
        for (int i = 0; i < 20; i++) {
            Phone phone = new Phone("用户"+(i+1));
            threadPoolExecutor.execute(phone);
        }
        
        threadPoolExecutor.shutdown();
    }
}

运行结果:

在这里插入图片描述

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;