您的当前位置:首页正文

Java多线程(六):线程池详解

2024-12-02 来源:个人技术集锦


1. 什么是线程池

        线程池(ThreadPool)是⼀种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在⼀个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。

2. 为什么要使用线程池

原因有以下几条:

        阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

        线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

3. 线程池创建

线程池的创建方法总共有 7 种,总体来说可分为 2 类:

  1. 通过 ThreadPoolExecutor 创建的线程池;
  2. 通过 Executors 创建的线程池。

线程池的创建方式总共包含以下 7 种(其中前 6 种是通过 Executors 创建的,最后 1 种是通过 ThreadPoolExecutor 创建的):

  1. Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后回收,若线程数不够,则新建线程;
  3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
  4. Executors.newScheduledThreadPool:创建⼀个可以执行延迟任务的线程池;
  5. Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
  6. Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定),根据当前CPU⽣成线程池【JDK1.8 添加】;
  7. ThreadPoolExecutor:手动创建线程池的方式,它包含了 7 个参数可供设置。

3.1 固定数量的线程池(Executors.newFixedThreadPool) 

3.1.1 创建固定数量的线程池

示例代码

public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        // 1.创建一个包含5个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 2.使用线程池执行任务
        for (int i = 0; i < 5; i++) {
            // 给线程池添加任务
            threadPool.submit(() -> System.out.println("线程名称:" + Thread.currentThread().getName()));
        }

        // 2.使用线程池执行任务2
        for (int i = 0; i < 10; i++) {
            // 给线程池添加任务
            threadPool.execute(() -> System.out.println("线程名称:" + Thread.currentThread().getName()));
        }
    }
}

 运行结果

3.1.2 线程池返回结果

示例代码

public class ThreadPoolDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);

        Future<Integer> future = threadPool.submit(() -> {
            int num = new Random().nextInt(100);
            System.out.println("生成随机数:" + num);
            return num;
        });
        System.out.println("得到线程池返回结果:" + future.get());
    }
}

 运行结果

3.1.3 submit() VS execut()

        使用线程池执行任务有两种方式:submit() 和 execut() 。这两种方式的区别如下:

        可以看到,使用 submit() 可以执行带有返回值的任务或者无返回值的任务,而 execut() 只能执行不带返回值的任务。

3.2.4 线程工厂

作用:为线程池提供现成的创建。

提供的功能

  1. 设置线程池中线程的命名规则;
  2. 设置线程优先级;
  3. 设置线程分组;
  4. 设置线程类型(守护线程 || 用户线程)。

示例代码

public class ThreadPoolDemo3 {
    public static void main(String[] args) {
        // 1.创建线程工厂
        ThreadFactory factory = r -> {
            // 一定要把任务 Runnable 设置给新线程
            Thread thread = new Thread(r);
            // 设置线程的命名规则
            thread.setName("我的线程:" + r.hashCode());
            // 设置线程的优先级
            thread.setPriority(Thread.MAX_PRIORITY);
            return thread;
        };
        ExecutorService service = Executors.newFixedThreadPool(5, factory);
        for (int i = 0; i < 5; i++) {
            service.submit(() -> {
                // 任务
                Thread thread = Thread.currentThread();
                System.out.println("线程池开始执行:" + thread.getName() + "线程池优先级:" + thread.getPriority());
            });
        }
    }
}

 运行结果

3.2 带缓存的线程池(Executors.newCachedThreadPool

        线程池会根据任务数创建线程,并且在一定时间内可以重复使用这些线程。

示例代码

public class ThreadPoolDemo4 {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            service.submit(() -> System.out.println("i:" + finalI + " 线程名称:" + Thread.currentThread().getName()));
        }
    }
}

 运行结果

 该方式适用于短时间有且有大量任务的场景,它的缺点是可能占用很多资源。

3.3 执行定时任务(Executors.newSingleThreadExecutor

3.3.1 延迟执行(1次)

示例代码

public class ThreadPoolDemo12 {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool =
                Executors.newScheduledThreadPool(10);
        // 定时任务
        System.out.println("设置定时任务:" + new Date());
        // 延迟 n 秒后执⾏(只执⾏⼀次)
        threadPool.schedule(() -> System.out.println("schedule:" + new Date()), 2, TimeUnit.SECONDS);
    }
}

执行结果

 延迟 2s 后执行一次。

3.3.2 固定频率执行( scheduleAtFixedRate

示例代码

public class ThreadPoolDemo13 {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool =
                Executors.newScheduledThreadPool(10);
        // 定时任务
        System.out.println("设置定时任务:" + new Date());
        threadPool.scheduleAtFixedRate(() -> 
                System.out.println("scheduleAtFixedRate:" + new Date()), 3, 2, TimeUnit.SECONDS);
    }
}

运行结果

延迟3s后执行,之后每2s执行一次。

参数解释

  1. 参数1:执行任务;
  2. 参数2:延迟 n 秒后执行;
  3. 参数3:执行定时任务的频率;
  4. 参数4:配合参数3使用的时间单位。

3.3.3 scheduleAtFixedRate VS scheduleWithFixedDelay

         scheduleAtFixedRate 是以上⼀次任务的开始时间,作为下次定时任务的参考时间的(参考时间+延迟任务=任务执行)。

示例代码

public class ThreadPoolDemo5 {
    public static void main(String[] args) {
        // 创建线程池
        ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
        System.out.println("添加任务执行时间:" + LocalDateTime.now());

        // 2s之后开始执行定时任务,定时任务每隔4s执行一次
        service.scheduleAtFixedRate(() -> {
            System.out.println("执行了任务:" + LocalDateTime.now());
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 2, 4, TimeUnit.SECONDS);
    }
}

运行结果

2s后开始执行定时任务,每隔5s执行一次。

设置的是每隔4秒执行一次定时任务,为什么实际上是5s执行一次呢? 

        注意,如果执行任务时间大于设置的定时任务执行时间,那么此方法会以执行任务的时间为准,简而言之,就是哪个时间长就以哪个时间作为定时任务执行的周期。

        

        scheduleWithFixedDelay 是以上⼀次任务的结束时间,作为下次定时任务的参考时间的。

示例代码

public class ThreadPoolDemo5 {
    public static void main(String[] args) {
        // 创建线程池
        ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
        System.out.println("添加任务执行时间:" + LocalDateTime.now());

        // 2s之后开始执行定时任务,每次执行间隔4秒
        service.scheduleWithFixedDelay(() -> {
            System.out.println("执行了任务:" + LocalDateTime.now());
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 2, 4, TimeUnit.SECONDS);
    }
}

运行结果

 2s后开始执行任务,每隔9秒执行一次定时任务。

 为什么这个也不是每隔4s执行一次,而是9s呢???

         因为  scheduleWithFixedDelay 是以上⼀次任务的结束时间,作为下次定时任务的参考时间的,上个任务执行5s后,再延时4s执行延时任务。

3.4 定时任务单线程(Executors.newSingleThreadScheduledExecutor

示例代码

public class ThreadPoolDemo6 {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        System.out.println("添加任务时间:" + LocalDateTime.now());
        service.schedule(() -> System.out.println("执行任务:" + LocalDateTime.now()), 2, TimeUnit.SECONDS);
    }
}

运行结果

3.5 单线程线程池(Executors.newSingleThreadExecutor

 示例代码

public class ThreadPoolDemo7 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            service.submit(() -> System.out.println("任务:" + finalI + ", 线程名:" + Thread.currentThread().getName()));
        }
    }
}

 运行结果

单线程的线程池有什么意义呢?

  1.  自定义拒绝策略;
  2. 提供了任务队列和任务管理的功能。

3.6 根据当前CPU生成线程池(Executors.newWorkStealingPool

示例代码

public class ThreadPoolDemo8 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newWorkStealingPool();
        for (int i = 0; i < 100; i++) {
            service.submit(() -> System.out.println("线程名:" + Thread.currentThread().getName()));
        }
        while (!service.isTerminated()){
        }
    }
}

运行结果

 

3.7 手动方式(ThreadPoolExecutor) 

3.7.1 创建忽略最新任务的线程池

示例代码

public class ThreadPoolDemo11 {
    public static void main(String[] args) {
        ThreadFactory factory = r -> {
            Thread thread = new Thread(r);
            return thread;
        };

        // 手动方式创建线程池
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(2, 2, 10, TimeUnit.SECONDS,
                        new LinkedBlockingDeque<>(2), factory, new ThreadPoolExecutor.DiscardPolicy());
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            executor.submit(() -> {
                try {
                    Thread.sleep(finalI * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行任务" + finalI);
            });
        }
        // 终止线程池
        executor.shutdown();
    }
}

运行结果

 

 3.7.2 ThreadPoolExecutor 参数说明

  1. corePoolSize核心线程数,可以大致理解为长期驻留的线程数目(除非设置了allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如newFixedThreadPool 会将其设置为 nThreads,而对于 newCachedThreadPool 则是为 0。
  2. maximumPoolSize顾名思义,就是线程不够时能够创建的最⼤线程数。同样进⾏对比,对于newFixedThreadPool,当然就是 nThreads,因为其要求是固定大小,而 newCachedThreadPool 则是 Integer.MAX_VALUE。
  3. keepAliveTime空闲线程的保活时间,如果线程的空闲时间超过这个值,那么将会被关闭。注意此值生效条件必须满足:空闲时间超过这个值,并且线程池中的线程数少于等于核⼼线程数corePoolSize。当然核心线程默认是不会关闭的,除非设置了allowCoreThreadTimeOut(true)那么核心线程也可以被回收。
  4. TimeUnit时间单位。
  5. BlockingQueue任务队列,用于存储线程池的待执行任务的。
  6. threadFactory⽤于生成线程,⼀般我们可以⽤默认的就可以了。
  7. handler当线程池已经满了,但是又有新的任务提交的时候,该采取什么策略由这个来指定。有几种方式可供选择,像抛出异常、直接拒绝然后返回等,也可以自己实现相应的接口实现自己的逻辑。

3.7.3 线程池执行流程

3.7.4 拒绝策略(5种(4(JDK提供的) + 1(自定义拒绝策略)))

 JDK提供的四种拒绝策略:

  1. DiscardPolicy : 忽略旧任务(队列第一个任务)
  2. AbortPolicy : 提示异常,拒绝执行(默认的拒绝策略)
  3. CallerRunsPolicy : 使用调用线程池的线程来执行任务
  4. DiscardOldestPolicy :  忽略最新任务 

自定义拒绝策略

示例代码

public class ThreadPoolDemo10 {
    public static void main(String[] args) {
        ThreadFactory factory = r -> {
            Thread thread = new Thread(r);
            return thread;
        };

        // 手动方式创建线程池
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(2, 2, 10, TimeUnit.SECONDS,
                        new LinkedBlockingDeque<>(2), factory,
                        new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                // 自定义拒绝策略
                                System.out.println("自定义拒绝策略");
                            }
                        });
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            executor.submit(() -> {
                try {
                    Thread.sleep(finalI * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行任务" + finalI);
            });
        }
        // 终止线程池
        executor.shutdown();
    }
}

运行结果:

 

4. 线程池状态

 查看 ThreadPoolExecutor 源码可知线程的状态如下:

  • RUNNING:这是最正常的状态:接受新的任务,处理等待队列中的任务;
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务;
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程;
  • TIDYING:所有的任务都销毁了,workCount 为 0。线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated(); 
  • TERMINATED:terminated() 方法结束后,线程池的状态就会变成这个。

各个状态的转换过程有以下几种:

  • RUNNING -> SHUTDOWN:当调用了 shutdown() 后,会发生这个状态转换,这也是最重要的
  • (RUNNING or SHUTDOWN) -> STOP:当调⽤ shutdownNow() 后,会发⽣这个状态转换;
  • SHUTDOWN -> TIDYING:当任务队列和线程池都清空后,会由 SHUTDOWN 转换为 TIDYING;
  • STOP -> TIDYING:当任务队列清空后,发⽣这个转换;
  • TIDYING -> TERMINATED:这个前面说了,当 terminated() 方法结束后。

shutdown VS shutdownNow :

  • shutdown 执行时线程池终止接收新任务,并且会将任务队列中的任务处理完;
  • shutdownNow 执行时线程池终止接收新任务,并且会给终止执行任务队列中的任务。

shutdown

public class ThreadPoolDemo10 {
    public static void main(String[] args) {
        ThreadFactory factory = r -> {
            Thread thread = new Thread(r);
            return thread;
        };

        // 手动方式创建线程池
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(2, 2, 10, TimeUnit.SECONDS,
                        new LinkedBlockingDeque<>(2), factory,
                        new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                // 自定义拒绝策略
                                System.out.println("自定义拒绝策略");
                            }
                        });
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            executor.submit(() -> {
                try {
                    Thread.sleep(finalI * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行任务" + finalI);
            });
        }
        // 终止线程池
        executor.shutdown();
    }
}

运行结果:

 shutdownNow 

5. 究竟选用哪种线程池

学习了这么多创建线程池的方式,究竟改用哪一种呢?

阿里巴巴《Java开发手册》给我们的答案:

        所以综上情况所述,我们推荐使用 ThreadPoolExecutor 的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避⼀些未知的风险。

显示全文