• 深度思考线程池面经之四:使用线程池的各种特性


    8 线程池(百度acg百度云一面)

    8.1 你是用到哪个线程池呢,在哪个场景中使用呢

    答:在秒杀系统中使用了newCachedThreadPool这个线程池

    8.2 自定义线程池的参数,你是怎么设置的呢

    8.3 写一个自定义线程池,要求核心线程数8,最大线程数10,有界队列10,现让你执行100个打印任务,同时还需要你自定义线程工厂,要求实现线程的优先级,所属线程组,线程异常处理策略,自定义线程名

    package demo;
    
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    
    // 自定义线程工厂
    class DIYThreadFactory implements ThreadFactory {
    
        public DIYThreadFactory(ThreadGroup threadGroup, String poolName, Integer priority, boolean isDaemon, Thread.UncaughtExceptionHandler handler) {
            this.threadGroup = threadGroup;
            this.threadPrefix = "pool-"+poolName+"-thread";
            this.priority = priority;
            this.isDaemon = isDaemon;
            this.handler = handler;
        }
    
        //线程名
        private final AtomicInteger atomicInteger = new AtomicInteger(0);
        // 线程分组
        private final ThreadGroup threadGroup;
    
        //线程前缀名
        private final String threadPrefix;
    
        private final Integer priority;
    
        // 是否后台线程
        private final boolean isDaemon;
    
        // 异常处理器
        private final Thread.UncaughtExceptionHandler handler;
    
    
        @Override
        public Thread newThread(Runnable r) {
            Thread t=new Thread(threadGroup,r,threadPrefix+atomicInteger.getAndIncrement(),0);
    
            if(t.isDaemon()!=isDaemon){
                t.setDaemon(isDaemon);
            }
            t.setPriority(priority);
            if(handler!=null){
                t.setUncaughtExceptionHandler(handler);
            }
            return t;
        }
    }
    // 自定义拒绝策略
    class DIYRejectStrategy implements RejectedExecutionHandler {
    
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("Task " + r.toString() + " rejected from " + executor.toString());
            // 这里你可以选择做其他的操作,例如记录日志、抛出异常、尝试重新添加任务等
        }
    }
    public class Test{
    
        private static final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingDeque<>(1000);
    
        public static void main(String[] args) {
    
            BlockingDeque<Runnable>bq=new LinkedBlockingDeque<>(10);
    
            Thread.UncaughtExceptionHandler handler=new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("Thread " + t.getName() + " encountered exception: " + e.getMessage());
                }
            };
    
            DIYThreadFactory diyThreadFactory=new DIYThreadFactory(null,"yxg",1,false,handler);
            ThreadPoolExecutor tpe=new ThreadPoolExecutor(2,10,10,TimeUnit.SECONDS,bq,diyThreadFactory,new DIYRejectStrategy());
    
            for (int i = 0; i < 100; i++) {
                final int fi = i;
                tpe.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (fi%5==0){
                            int i1 = fi / 0;
                        }
                        System.out.println("send "+ fi +" th msg");
                    }
                });
            }
            tpe.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    8.3.1 下面这道报错代码怎么解决

    for (int i = 0; i < 100; i++) {
    
        tpe.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("send "+ finalI +" th msg");
            }
        });
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    你在匿名内部类中尝试访问循环变量i。在Java 8之前,当你在匿名内部类中使用一个外部变量,那个变量必须是final的。从Java 8开始,你可以在lambda表达式或匿名内部类中使用所谓的“effectively final”的变量,即它不必明确地声明为final,但你也不能在其后更改它。

    你的代码中,变量i在循环中是会改变的,所以不能直接在匿名内部类中使用。你之前的注释的代码(int finalI = i;)是正确的,因为你在创建了一个effectively final的局部变量finalI,并将i的值赋给它。

    解决方案:

    1. 使用一个effectively final的局部变量来传递i的值给匿名内部类。
    for (int i = 0; i < 100; i++) {
        int finalI = i;
        tpe.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("send "+ finalI +" th msg");
            }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 或者,你可以使用Java 8的lambda表达式,这样会更简洁:
    for (int i = 0; i < 100; i++) {
        int finalI = i;
        tpe.execute(() -> System.out.println("send "+ finalI +" th msg"));
    }
    
    • 1
    • 2
    • 3
    • 4

    在这两种解决方案中,我们都使用了局部变量finalI来在匿名内部类或lambda表达式中使用循环变量i

    8.3.2 什么是effective final

    "effectively final"是Java 8引入的一个概念,它允许你在匿名内部类或lambda表达式中引用没有被明确标记为final的局部变量,但有一个限制:这个局部变量在赋值后不能再次被修改。

    简单地说,一个局部变量如果满足以下条件,它就是"effectively final"的:

    1. 它在初始化之后不再被赋新的值。
    2. 它不需要明确地使用final关键字来修饰。

    例如:

    int x = 10;
    // some code...
    x = 20;  // After this assignment, 'x' is no longer effectively final
    
    • 1
    • 2
    • 3

    如果你去掉x = 20;这行代码,即便x没有被明确地标记为final,它仍然是"effectively final"的,因为在初始化后它没有被赋新的值。

    这个概念的引入是为了让Java的语法更加灵活,尤其是在使用lambda表达式时。在Java 8之前,如果你想在匿名内部类中引用一个局部变量,那么这个变量必须被明确地声明为final。而在Java 8及其后续版本中,只要局部变量满足"effectively final"的条件,即使它没有被明确地声明为final,你也可以在lambda表达式或匿名内部类中使用它。

    8.4 使用线程池时,设置各项参数的依据有哪些(重要)

    答:我的回答:qps(任务数),任务的执行时间,机器资源的限制

    使用线程池时,设置参数要根据具体应用场景和需求来决定。以下是设置线程池参数时需要考虑的一些常见场景和因素:

    1. 任务的性质

      • 计算密集型任务:这些任务主要消耗CPU资源。线程池的大小应该接近于系统的可用处理器的数量,这样可以最大程度地利用CPU资源。

      • I/O密集型任务:这些任务可能需要等待外部资源,如文件系统、数据库或网络。由于线程经常被阻塞,可以配置一个线程数大于处理器数量的线程池,这样可以保持处理器始终在工作。

      • 混合型任务:当有计算和I/O混合的任务时,需要对线程池的大小进行细致的调整。

    2. 任务的持续时间和频率

      • 长时间运行的任务:这可能会使线程池中的所有线程都被占用,导致新任务等待。对于这种情况,可能需要一个更大的线程池。

      • 短暂、频繁的任务:可缓存的线程池可能更为适用。

    3. 任务的优先级

      • 一些任务可能比其他任务更为紧急。虽然Java线程池本身不支持优先级队列,但可以通过自定义队列和任务来模拟。
    4. 任务间的依赖关系

      • 如果任务B依赖于任务A的结果,这可能会引入死锁风险。这种情况下,需要特别注意线程池的大小和队列策略。
    5. 资源限制

      • 如果系统有限的资源(如数据库连接),过多的并发请求可能导致资源耗尽。这时,线程池的大小和队列的容量可能需要限制以防止资源竞争。
    6. 响应时间要求

      • 如果系统对响应时间有严格的要求,可能需要更多的线程来确保任务能够快速开始执行,而不是在队列中等待。
    7. 拒绝策略

      • 当队列和线程池都满时,应如何处理新提交的任务?是否应该抛出异常、丢弃任务、或使提交任务的线程自己来执行任务?
    8. 线程池的生命周期和管理

      • 如果应用经常启动和关闭,固定大小的线程池可能更适合,因为它们在空闲时不会关闭线程。但是,如果线程池的生命周期很长,并且任务有高峰和低谷,缓存线程池可能更为合适,因为它可以在低谷时回收线程。

    考虑这些场景和因素,可以帮助设计一个高效、响应迅速并且资源利用率高的线程池。但仍建议进行性能测试和监控,以确保所选参数在实际生产环境中的表现符合预期。

    8.5 基于各种场景,你会怎么使用线程池呢,参数怎么设置

    8.6 线程优先级内部的原理是怎么实现的,高优先级的线程是先执行吗?难道任务队列内部会对他们进行排序吗

    线程的优先级是与操作系统级的线程调度有关的,Java的线程优先级映射到操作系统的线程优先级。线程调度机制的实现是操作系统依赖的,所以Java线程优先级的行为也可能因操作系统而异。以下是一些一般的概念:

    • 优先级为提示:线程优先级在Java中主要作为线程调度的提示。高优先级的线程在大多数情况下会比低优先级的线程得到更多的执行机会,但这并不是一个严格的保证。
    • 不同的调度策略:大多数现代操作系统使用优先级的抢先式调度(preemptive scheduling)机制,即任何时候只要有一个更高优先级的线程就绪,当前正在执行的低优先级线程就会被打断。
    • 没有排序操作系统的线程调度器不会持续地重新排序就绪线程。而是在选择下一个要执行的线程时,根据优先级考虑它们。

    8.7 操作系统的线程调度器不会持续地重新排序就绪线程。而是在选择下一个要执行的线程时,根据优先级考虑它们。 但是java是不是可以自定义一个优先级队列,实现任务的重排序的功能?(重要)

    是的,Java确实提供了工具来自定义优先级队列,并通过它实现任务重排序功能。java.util.PriorityQueue 是Java标准库中的一个类,它实现了一个优先队列。这个队列内部是使用堆(heap)数据结构来实现的,这使得每次从队列中取出的元素都是具有最高优先级的。

    如果你想在多线程环境中,比如线程池中,使用基于优先级的任务调度,可以结合 PriorityQueueThreadPoolExecutor 来实现。你可以提供一个自定义的 BlockingQueue(例如,基于 PriorityQueue 的实现)给 ThreadPoolExecutor,并确保任务对象实现了 Comparable 接口,或者在构建 PriorityQueue 时提供一个 Comparator

    下面是一个简单示例,展示了如何使用 PriorityQueueThreadPoolExecutor 提供基于优先级的任务调度:

    import java.util.concurrent.*;
    
    public class PriorityTask implements Runnable, Comparable<PriorityTask> {
        private int priority;
        private String name;
    
        public PriorityTask(int priority, String name) {
            this.priority = priority;
            this.name = name;
        }
    
        @Override
        public void run() {
            System.out.println("Executing: " + name);
        }
    
        @Override
        public int compareTo(PriorityTask o) {
            return Integer.compare(o.priority, this.priority); // higher values mean higher priority here
        }
    
        public static void main(String[] args) {
            BlockingQueue<PriorityTask> priorityQueue = new PriorityBlockingQueue<>();
    
            ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, priorityQueue);
    
            executor.submit(new PriorityTask(1, "low-priority-task"));
            executor.submit(new PriorityTask(3, "medium-priority-task"));
            executor.submit(new PriorityTask(2, "high-priority-task"));
    
            executor.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在上述示例中,任务会按优先级从高到低执行。但是,你应该小心使用基于优先级的调度,因为它可能会导致低优先级的任务饥饿,即在极端情况下,低优先级的任务可能永远得不到执行的机会。

    8.7.1 任务的优先级和线程的优先级是不同的,对吧

    是的,任务的优先级和线程的优先级是两个不同的概念。让我们详细区分它们:

    1. 线程的优先级

      • 线程优先级是一个整数,通常在操作系统中,每个线程都有一个与之相关的优先级。

      • 在Java中,线程优先级可以使用Thread.setPriority(int)方法进行设置,范围从Thread.MIN_PRIORITY (即值1) 到 Thread.MAX_PRIORITY (即值10)。默认的优先级是Thread.NORM_PRIORITY (即值5)。

      • 线程的优先级是给线程调度器的一个提示,告诉调度器哪些线程更应该得到执行的机会。但是,具体的行为取决于操作系统的线程调度策略。

      • 在某些系统中,线程优先级可能并不会严格地被考虑;而在其他系统中,它可能会被严格地遵循。因此,依赖线程优先级可能会导致跨平台的不可预测行为。

    2. 任务的优先级

      • 任务的优先级通常用于在基于优先级的队列中对任务进行排序,如上文中所示的PriorityBlockingQueue

      • 任务的优先级通常与具体的应用逻辑有关,例如,在一个任务队列中,某些任务由于其紧迫性或重要性可能被赋予较高的优先级。

      • 当使用线程池和优先级任务队列时,线程池中的工作线程将按照任务的优先级来选择任务执行。但这与线程自身的优先级无关。

      • 使用任务优先级时要小心,以避免低优先级任务的饥饿。

    总结起来,线程优先级与操作系统的线程调度相关,而任务优先级与如何从队列中选择要执行的任务相关。在设计系统时,应明确考虑这两种优先级的影响和它们的使用场景。

    8.7.2 java线程池如何实现任务优先级

    1 写法一
    import java.util.concurrent.*;
    
    public class PriorityTask implements Runnable, Comparable<PriorityTask> {
        private int priority;
        private String name;
    
        public PriorityTask(int priority, String name) {
            this.priority = priority;
            this.name = name;
        }
    
        @Override
        public void run() {
            System.out.println("Executing: " + name);
        }
    
        @Override
        public int compareTo(PriorityTask o) {
            return Integer.compare(o.priority, this.priority); // higher values mean higher priority here
        }
    
        public static void main(String[] args) {
            PriorityBlockingQueue<PriorityTask> priorityQueue = new PriorityBlockingQueue<>();
    
            ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, priorityQueue);
    
            executor.submit(new PriorityTask(1, "low-priority-task"));
            executor.submit(new PriorityTask(3, "medium-priority-task"));
            executor.submit(new PriorityTask(2, "high-priority-task"));
    
            executor.shutdown();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    2 写法二: 推荐
    import java.util.concurrent.*;
    
    public class PriorityTask implements Runnable{
        private int priority;
        private String name;
    
        public PriorityTask(int priority, String name) {
            this.priority = priority;
            this.name = name;
        }
    
        @Override
        public void run() {
            System.out.println("Executing: " + name);
        }
    
    
        public static void main(String[] args) {
            PriorityBlockingQueue<PriorityTask> priorityQueue = new PriorityBlockingQueue<>(10,(o1, o2)->(o2.priority-o1.priority));
    
            ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, priorityQueue);
    
            executor.submit(new PriorityTask(1, "low-priority-task"));
            executor.submit(new PriorityTask(3, "medium-priority-task"));
            executor.submit(new PriorityTask(2, "high-priority-task"));
    
            executor.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
  • 相关阅读:
    何为消息队列?它的特点是什么?
    YOLOv8改进实战 | 更换主干网络Backbone(五)之2023最新轻量化主干网络VanillaNet,深度学习中极简主义的力量
    20220727NOI模拟赛--考后总结
    为什么网上的流量卡都有禁发地区呢?流量卡管控地区整理!
    Python的分子模拟动态促进DF Theory理论对二进制硬盘系统的适用性
    使用 Netty 实现简易版 Dubbo RPC 远程调用过程
    2023年03月 Scratch(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
    每日算法----464. 我能赢吗----2022/05/22
    Kubernetes:健康检查
    前端 JS 安全对抗原理与实践
  • 原文地址:https://blog.csdn.net/yxg520s/article/details/133818559