【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题

发布于:2024-04-20 ⋅ 阅读:(17) ⋅ 点赞:(0)

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱
ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客
本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶
个人主页xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:xiaoxie的JAVAEE学习系列专栏——CSDN博客●'ᴗ'σσணღ
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)!

目录

一.阻塞队列

1.什么是阻塞队列

2.什么是生产者-消费者模型

3.使用代码简单演示一下生产者消费者模型

 4.使用代码模拟一个阻塞队列

二.线程池

1.什么是线程池(面试题)

2.为什么要在程序中使用线程池?(面试题)

3.线程池有哪些重要的参数?(面试题)

4.线程池常用的拒绝策略解析

AbortPolicy (中止策略)

CallerRunsPolicy (调用者运行策略)

DiscardPolicy (丢弃策略)

DiscardOldestPolicy (丢弃最老策略)

5..Java中如何创建线程池?(面试题)

1. 使用Executors工厂类创建线程池

2. 使用ThreadPoolExecutor类手动创建线程池

3. 使用Executors工厂类创建定时任务线程池

6.使用线程池是否会引入新的并发问题,比如死锁、线程饥饿等,应该如何避免这些问题?(面试题)

7.线程池的工作流程 ?

8.如何在项目中使用线程池?有哪些线程池的最佳实践和注意事项

9.线程池中的线程都有哪些状态(考察线程都有哪些状态)

10.如何获取线程池的状态信息(如线程数、任务队列大小等)?

11.如何正确关闭线程池以防止资源泄漏?shutdown()和shutdownNow()方法的区别是什么?

12.如何结合Future和Callable接口实现有返回值的任务?

13. 对于定时任务,如何使用ScheduledThreadPoolExecutor


一.阻塞队列

1.什么是阻塞队列

阻塞队列(BlockingQueue)是一种特殊的队列,在Java等编程语言中广泛应用于多线程同步与通信,它是一个线程安全的数据结构,主要用于在多线程环境下存储和传递数据。阻塞队列的关键特性在于,当队列处于特定状态时,对队列的插入(生产)或删除(消费)操作能够自动让线程进入等待状态。

具体来说,阻塞队列具有以下特点:

  1. 当队列为空时,执行从队列中获取元素(如调用take()方法)的线程会被阻塞,直到其他线程向队列中放入元素使其变为非空。
  2. 当队列已满时,尝试向队列中添加元素(如调用put()方法)的线程也会被阻塞,直至队列中有空间可供添加新的元素。

注意:阻塞队列的api中使用添加元素只有使用put()方法获取元素take()方法才有阻塞功能.

这种设计使得生产者线程(负责向队列中添加元素)和消费者线程(负责从队列中移除元素)能够协调工作,避免了无谓的CPU轮询,从而提高了系统的资源利用率和并发性能。

Java中的java.util.concurrent包提供了多种阻塞队列实现,例如:

  • ArrayBlockingQueue:基于数组实现的有界阻塞队列。
  • LinkedBlockingQueue:基于链表实现的可选有界阻塞队列,默认容量为Integer.MAX_VALUE。
  • PriorityBlockingQueue:带优先级排序的无界阻塞队列。
  • DelayQueue:基于优先级队列,元素只有在其指定延迟时间到了之后才能从队列中取出。
  • SynchronousQueue:一种特殊的无缓冲阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。
  • LinkedTransferQueue:一个无界的链表结构阻塞队列,还支持直接转移操作。
  • LinkedBlockingDeque:双端阻塞队列,可以在两端进行插入和移除操作。 

阻塞队列通常配合线程池或其他并发框架,用于构建复杂的多线程生产者-消费者模型

2.什么是生产者-消费者模型

生产者消费者模型(Producer-Consumer Model)是计算机科学中的一种经典设计模式,主要应用于多线程编程或者并发程序设计中,用来描述两个不同角色的线程如何有效地共享和交换数据。该模型由两部分组成:

生产者(Producer): 生产者是指在系统中负责生成或提供数据的一方。它可以是一个线程、进程或函数,其任务是不断地生成数据并将这些数据放入到一个共享的数据结构(通常是队列)中。

消费者(Consumer): 消费者则是指负责消耗或处理数据的一方。同样可以是一个线程、进程或函数,它的任务是从共享的数据结构中取出生产者放入的数据,并进行相应的处理或消费。

阻塞队列(Blocking Queue): 生产者消费者模型的核心是引入了一个中介——阻塞队列。生产者将产生的数据放入队列时,若队列已满,则生产者线程会阻塞等待,直到队列中有空间可用。反之,消费者从队列中取出数据时,若队列为空,则消费者线程会被阻塞,直到队列中有数据可消费。

通过这种方式,生产者和消费者无需直接交互,而是通过队列这一“缓冲区”间接地进行数据交换,从而降低了它们之间的耦合度。同时,阻塞队列机制也确保了系统的稳定性和效率,因为它能够有效防止因生产速度过快导致数据丢失或消费者处理速度过慢造成资源浪费的问题,达到平衡生产和消费速率的目的。此外,这种模型还能更好地支持并发处理和异步逻辑,有助于提升系统整体性能和扩展性。

简单画一个图来解释一下生产者消费者模型和阻塞队列之间的联系

该图表示的就是一个简单的生产者消费者模型,线程1(生产者) 产生了数据,写入到阻塞队列中去,线程2(消费者)从阻塞队列中读取数据,就不需要两个线程之间进行数据交互,线程1就不需要和线程2的代码之间有所联系,哪怕未来因为业务的需求对线程1有所修改,也不会对线程2有什么影响,降低了两个线程的耦合性,在加上阻塞队列具有如果队列满的话,就阻塞等待,等消费者,读取之后,生产者才会继续写入数据到阻塞队列中,就防止了,假如有一些突发的状况,导致线程1的请求突增,导致线程1的生产数据量特别多,但由于有阻塞队列这个缓冲区,再加上阻塞队列的队满就阻塞等待的特点,线程2就可以保持现有的节奏读取(消费)数据.阻塞队列就像一个天然的流量控制器,它能自动调节数据的生产和消费速度,使得系统能在高负载时“吸收”过多的数据,并在低负载时按需提供数据,从而实现更稳定的系统运行状态。

总结来说有下列几个特点

  1. 解耦:生产者和消费者之间通过共享的数据结构(通常是队列或缓冲区)进行通信,不需要直接相互调用对方的代码,从而实现了高度的解耦。

  2. 异步处理:生产者产生数据后直接放入共享缓冲区,无需等待消费者处理;消费者从缓冲区取出数据进行处理,两者各自独立运行,支持异步并发执行。

  3. 同步与互斥:生产者和消费者都需要同步机制来确保在缓冲区满或空的情况下不会发生错误操作,例如使用互斥锁和条件变量来保证在缓冲区满时生产者阻塞,缓冲区空时消费者阻塞。

  4. 资源利用率优化:通过缓冲区的存在,能够平衡生产者和消费者的处理速率差异,允许系统在瞬态负载不均衡时依然高效运行,避免了生产者过快造成数据丢失或消费者过慢导致生产者阻塞。

  5. 并发控制:生产者和消费者可能存在于多个线程中,因此模型中会涉及多线程间的同步和并发控制,比如使用信号量、Condition、BlockingQueue等机制。

  6. 容错与稳定性:阻塞队列的设计有助于处理突发流量,能够在流量高峰时存储多余数据,避免数据溢出,同时在流量较低时稳定地消耗数据,增强了系统的稳定性和健壮性。这里可以联想一下三峡大坝

  7. 扩展性:由于生产者和消费者职责明确且相对独立,这种模型便于系统组件的添加、删除或升级,易于进行分布式架构的扩展。

3.使用代码简单演示一下生产者消费者模型

public class Demo {
    public static void main(String[] args) {
        BlockingQueue<Integer> q = new LinkedBlockingQueue<>();//阻塞队列
        Thread t1 = new Thread(()-> {
            int count = 0;
           while (true) {
               try {
                   q.put(count);
                   System.out.println("t1生产了: " + count);
                   Thread.sleep(1000);//让我们可以看清生产者消费的过程
                   count++;
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        Thread t2 = new Thread(()-> {
            while (true) {
                try {
                    int tmp = q.take();
                    System.out.println("t2消费了: " + tmp);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

 结果:

这段Java代码创建了一个简单的消费者生产者模型实例。程序中定义了一个主线程main,并在其中初始化了一个LinkedBlockingQueue类型的阻塞队列。然后创建了两个线程t1t2分别作为生产者和消费者:

  • 生产者线程t1在一个无限循环中,不断将自增计数器count的值放入阻塞队列q中,每次放入后输出“t1生产了: ”以及当前计数值,并休眠1秒以便观察效果。当队列已满时,q.put(count)会阻塞生产者线程,直到队列中有空闲位置。

  • 消费者线程t2也在一个无限循环中,不断从阻塞队列q中取出元素,并输出“t2消费了: ”以及消费的元素值。当队列为空时,q.take()会阻塞消费者线程,直到队列中有新的元素可供消费。

整个程序启动后,生产者线程t1持续生成整数并将其放入队列,而消费者线程t2则不断地从队列中取出并消费这些整数。由于使用了阻塞队列,所以生产者和消费者之间的协同工作得以简化,并且自然地实现了生产速度与消费速度的匹配.

队列满的时候的情况

public class Demo {
    public static void main(String[] args) {
        BlockingQueue<Integer> q = new LinkedBlockingQueue<>(1000);//阻塞队列并给定队列的容量
        Thread t1 = new Thread(()-> {
            int count = 0;
           while (true) {
               try {
                   q.put(count);
                   System.out.println("t1生产了: " + count);
                   count++;
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        Thread t2 = new Thread(()-> {
            while (true) {
                try {
                    int tmp = q.take();
                    System.out.println("t2消费了: " + tmp);
                    Thread.sleep(1000);//模拟队列满的情况
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

结果

这个修改过的Java代码与之前提供的版本类似,同样实现了一个基于LinkedBlockingQueue的生产者-消费者模型。不同之处在于:

  1. 在创建阻塞队列LinkedBlockingQueue时,现在指定了初始容量为1000,这意味着队列最多能存储1000个元素。

  2. 消费者线程t2在取走队列中的元素后,增加了一行Thread.sleep(1000)来模拟消费过程耗时较长的情况。这意味着每消费一个元素,消费者线程都会暂停1秒。

结合这两点改动,当生产者的生产速度超过消费者的速度时(例如生产间隔小于消费耗时),如果队列已满(达到1000个元素),那么尝试继续向队列中添加元素的生产者线程t1将会被阻塞,直到消费者线程从队列中取出一个元素腾出空间为止。

这样,该程序展示了如何通过LinkedBlockingQueue来协调生产和消费的速度,防止数据丢失或者过度占用内存资源的问题。不过请注意,在实际应用中,为了确保程序能够正常退出,通常需要设计一种机制让生产者和消费者在适当的时候停止运行。在这个示例中,因为生产者和消费者都在无限循环中,因此它们会在队列满且无法继续放入元素或程序被外部强制中断时,才会结束运行。

 4.使用代码模拟一个阻塞队列

这里使用代码自己模拟一个简单的阻塞队列,主要就是为了可以更好的理解阻塞队列的功能

// 定义一个简单的阻塞队列类 MyBlockQueue,使用 int 数组模拟队列
class MyBlockQueue {
    // 使用数组存储队列中的元素
    private int[] elem;
    
    // 使用 volatile 关键字修饰 head 和 trial 变量,保证多线程环境下的内存可见性和防止指令重排序
    private volatile int head;
    private volatile int trial;
    private volatile int sz; // 记录队列中元素的数量

    // 构造函数,传入队列的容量
    public MyBlockQueue(int capacity) {
        this.elem = new int[capacity];
    }

    // 生产者方法:向队列中添加元素
    public void put(int val) throws InterruptedException {
        synchronized (this) { // 使用同步块保证线程安全
            // 当队列满时,生产者线程等待
            while (sz >= elem.length) {
                wait();
            }
            
            // 将元素放入队列,更新 trial 指针
            elem[trial++] = val;
            if (trial >= elem.length) {
                trial = 0; // 如果超过数组长度,循环回到数组起始位置
            }
            sz++; // 队列大小加一
            
            // 添加完元素后,唤醒等待的消费者线程
            notify();
        }
    }

    // 消费者方法:从队列中取出元素
    public int take() throws InterruptedException {
        synchronized (this) { // 同样使用同步块保证线程安全
            // 当队列空时,消费者线程等待
            while (sz == 0) {
                wait();
            }
            
            // 从队列中取出元素,更新 head 指针
            int tmp = elem[head++];
            if (head >= elem.length) {
                head = 0; // 如果超过数组长度,循环回到数组起始位置
            }
            sz--; // 队列大小减一
            
            // 取出元素后,唤醒等待的生产者线程
            notify();
            
            // 返回取出的元素
            return tmp;
        }
    }
}

// 主类 Demo,用于测试 MyBlockQueue 类
public class Demo {
    public static void main(String[] args) {
        // 创建一个容量为 1000 的自定义阻塞队列实例
        MyBlockQueue q = new MyBlockQueue(1000);

        // 创建生产者线程 t1,持续向队列中添加元素
        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    q.put(count);
                    System.out.println("t1生产了: " + count);
                    count++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        // 创建消费者线程 t2,持续从队列中取出并打印元素
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    int tmp = q.take();
                    System.out.println("t2消费了: " + tmp);
                    Thread.sleep(1000); // 模拟消费过程,使生产与消费的速度差异明显
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        // 启动生产者和消费者线程
        t1.start();
        t2.start();
    }
}

结果为:

此代码实现了一个基于数组的简单阻塞队列,通过synchronized关键字和wait()notify()方法实现了线程间的同步和通信,确保了在多线程环境下生产者和消费者线程的安全协作。在主类Demo中创建并启动了生产者线程t1和消费者线程t2,以此展示生产者-消费者模型的运作.

二.线程池

线程池作为多线程编程中十分重要的一个内容,在我们日常开发以及面试中都是常用与常考的内容,博主这里就通过一些线程池面试常考的题目来解析线程池

1.什么是线程池(面试题)

线程池是一种多线程处理形式,它预先创建了一组可重用的线程,这些线程在完成任务后并不会立即销毁,而是重新等待新的任务到来。当应用程序需要执行新的任务时,可以直接从线程池中获取已经存在的空闲线程来执行,而非每次都创建新线程,从而降低系统资源的消耗和上下文切换的开销。

举个例子:

想象一个生产玩具的工厂,这个工厂有若干条装配线,每条装配线都有多个工人。这些工人可以看作是线程池中的线程。工厂每天都会收到来自不同客户的订单,订单上列出了需要生产的玩具类型和数量。这就像程序中的各种任务,需要被执行。

  1. 避免重复准备资源:如果每次接到订单,工厂都要临时招募工人、搭建新的装配线,这会消耗很多时间和资源。而有了固定的工人和装配线,工厂可以快速响应新的订单,因为工人和装配线已经是准备好的。这与线程池避免频繁创建和销毁线程的开销是类似的。

  2. 任务调度和执行:工厂的管理者会根据每个工人的技能和当前的工作负载来分配订单。如果某个工人正在忙碌,管理者会将新订单分配给空闲的工人。线程池也通过一个调度器来管理线程和任务,确保任务能够高效地被执行。

  3. 资源的合理分配:工厂会根据订单的数量和复杂度来调整生产线的运行速度和工人的数量,以确保既能满足生产需求,又不会造成资源浪费。线程池也会根据系统的需求和工作负载来动态调整线程的数量,以达到最优的资源利用。

  4. 处理突发情况:在工厂中,如果突然接到一个大型订单,管理者可以临时增加工人数量或加班来应对。线程池也有类似的机制,可以在任务量突增时,通过增加线程数量或使用其他策略来保证任务的及时完成。

通过这个工厂流水线的例子,我们可以更直观地理解线程池如何提高任务处理的效率、合理分配资源以及灵活应对不同工作负载的情况。线程池的设计使得多任务处理变得更加高效和稳定,是现代软件开发中不可或缺的一部分。

2.为什么要在程序中使用线程池?(面试题)

  1. 提高性能每次创建和销毁线程都会带来一定的系统开销,特别是当频繁进行这些操作时。线程池通过重用已经创建的线程来避免这些开销,从而提高程序的执行效率。线程创建涉及到操作系统内核的调用,这是一个相对耗时的过程,而线程池中的线程可以在任务间快速切换(用户态),减少了这种开销。

  2. 控制资源消耗:线程池允许开发者设定最大线程数量,这样可以避免因为线程数量过多而导致的系统资源耗尽。通过限制线程数量,线程池有助于防止系统过载,提高系统的稳定性和可靠性

  3. 简化编程模型:管理线程的创建、执行和销毁是一个复杂的过程,使用线程池可以简化这一过程。开发者只需将任务提交给线程池,线程池会自动处理线程的创建、执行和回收,这样开发者可以专注于业务逻辑的实现,提高开发效率。

  4. 更好的资源利用率线程池可以根据系统的负载动态调整线程数量,确保CPU等资源得到合理利用。在任务较少时,线程池可以减少活跃线程的数量以节省资源;在任务较多时,线程池可以增加线程数量以提高处理能力。

  5. 提高系统的响应速度:由于线程池中的线程是预先创建类似于单例模式的饿汉模式的,当有新任务到来时,线程池可以立即分配线程来执行任务,而不需要等待线程的创建,这可以减少任务的响应时间。

  6. 增强程序的可维护性:线程池通常提供了丰富的任务调度策略和监控机制,如优先级队列、任务拒绝策略等,这些机制可以帮助开发者更好地控制和管理程序的行为,使得程序更加健壮和易于维护。

  7. 容错和错误隔离:线程池可以配合错误处理机制,如超时、重试等策略,来提高程序的容错能力。当某个任务失败时,不会影响整个程序的运行,线程池可以继续执行其他任务,或者重新尝试失败的任务。

3.线程池有哪些重要的参数?(面试题)

在Java中,线程池的配置和性能很大程度上取决于几个重要的参数。这些参数可以通过ThreadPoolExecutor进行设置,它们分别是:

  1. corePoolSize核心线程数。这是线程池中始终保持的线程数量,即使这些线程处于空闲状态,除非设置了allowCoreThreadTimeOut

  2. maximumPoolSize最大线程数。这是线程池中允许的最大线程数量。当工作队列满了且当前运行的线程数小于最大线程数时,线程池会创建新线程来处理任务。

  3. keepAliveTime非核心线程的空闲存活时间。这是非核心线程(即核心线程以外的线程)在终止前等待新任务的最长时间。

  4. unitkeepAliveTime的时间单位。常用的时间单位有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。

  5. workQueue任务的等待队列。这是一个用于存放等待执行的任务的队列。常用的队列类型有LinkedBlockingQueueArrayBlockingQueueSynchronousQueue等。

  6. threadFactory线程工厂。用于创建新线程的工厂类。可以通过自定义ThreadFactory来控制线程的创建过程,如设置线程的名称、优先级、是否为守护线程等。

  7. handler拒绝策略。当工作队列满了且线程数达到最大线程数时,新提交的任务如何处理的策略。常用的拒绝策略 (该问题的重点,后续会展开说明)

  8. ThreadPoolExecutor.AbortPolicy抛出异常)     ThreadPoolExecutor.CallerRunsPolicy调用者运行任务)                                               ThreadPoolExecutor.DiscardPolicy(丢弃任务)ThreadPoolExecutor.DiscardOldestPolicy(丢弃队列最前面的任务)                               

这些参数共同决定了线程池的行为和性能。合理配置这些参数对于创建高效、稳定的线程池至关重要。例如,过小的核心线程数可能导致任务处理不及时,而过大的最大线程数可能会消耗过多的系统资源。工作队列的选择和配置也会影响任务的处理方式和线程池的吞吐量。拒绝策略的选择则决定了当线程池无法处理更多任务时的处理方式。

在实际应用中,需要根据任务的特性、系统的资源和性能要求来调整这些参数,以达到最优的性能和资源利用率。

4.线程池常用的拒绝策略解析

  1. AbortPolicy (中止策略)
    • 行为:当任务被拒绝时,直接抛出RejectedExecutionException异常。
    • 优点:明确告知调用者线程池无法处理新的任务,让调用者有机会捕获异常并采取相应措施,比如记录错误日志、调整任务提交速率或采用其他处理方式。
    • 缺点:如果没有妥善处理异常,可能导致应用程序中断或者不可预期的行为。
    • 应用场景:适合需要及时得知任务拒绝情况并对异常情况进行特殊处理的场景。
  2. CallerRunsPolicy (调用者运行策略)
    • 行为:由提交任务的线程(调用execute()方法的线程)自己来执行被拒绝的任务。
    • 优点:可以减缓新任务提交的速度,因为提交任务的线程必须等待自己的任务执行完毕才能继续提交新的任务,有助于防止任务过度积压和减轻线程池的压力。
    • 缺点:可能会影响提交任务的线程原本的执行流程,而且如果连续提交大量任务,可能会使调用者线程陷入繁忙状态,无法处理其他重要的同步操作。
    • 应用场景:适用于提交任务的线程本身有能力处理额外负载且希望自我限制任务提交速度的场合。
  3. DiscardPolicy (丢弃策略)
    • 行为:静默丢弃被拒绝的任务,不做任何处理,也不会抛出异常。
    • 优点:简单粗暴,不会影响线程池的正常运行,适合于任务相对不重要,可以容忍偶尔丢失的情况。
    • 缺点:由于没有任何反馈机制,用户无法得知任务已被丢弃,可能会造成数据丢失或业务逻辑的不完整执行。
    • 应用场景:在任务可选或可以安全丢弃的情况下,如实时性要求不高或任务可以稍后重试的情境。
  4. DiscardOldestPolicy (丢弃最老策略)
    • 行为:移除工作队列中等待时间最长的任务(通常是FIFO队列的首部任务),然后尝试重新提交被拒绝的任务。
    • 优点:尝试给新任务腾出空间,有利于处理新近提交的重要任务。
    • 缺点:可能会取消一些已经在队列中等待很久的任务,这对任务的公平性和一致性是有损的,且不能保证新提交的任务一定能被接受。
    • 应用场景:在任务具有时效性且倾向于处理最新提交任务的场景下,可以考虑使用,但仍需谨慎评估可能带来的后果。

同时也可以自定义拒绝策略实现RejectedExecutionHandler接口,以满足特定的业务需求。 

5..Java中如何创建线程池?(面试题)

在Java中,创建线程池通常涉及到java.util.concurrent包中的ExecutorService接口及其实现类。最常用的实现类有ThreadPoolExecutorExecutors工厂类提供的几种快捷创建线程池的方法。以下是如何在Java中创建线程池的几种方法:

1. 使用Executors工厂类创建线程池

Executors提供了一些静态工厂方法,可以方便地创建不同类型的线程池:

  • 固定大小的线程池Executors.newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,该线程池的核心线程数最大线程数都等于 nThreads
int numberOfThreads = 10;
ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);

  • 可缓存的线程池Executors.newCachedThreadPool() 创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求,将回收空闲(60秒不执行任务)的线程。
ExecutorService executorService = Executors.newCachedThreadPool();
  • 单线程的执行器Executors.newSingleThreadExecutor() 创建一个单线程的执行器,它只会创建一个线程来执行任务,保证了所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
  • 使用ScheduledExecutorServiceExecutors.newScheduledThreadPool(int corePoolSize) 创建一个大小无限的线程池,支持定时任务和周期性任务。
int corePoolSize = 5;
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(corePoolSize);

2. 使用ThreadPoolExecutor类手动创建线程池

如果需要更细致地控制线程池的参数,可以直接使用ThreadPoolExecutor的构造函数来创建线程池:

// 定义线程池的核心线程数量
int corePoolSize = 5;

// 定义线程池的最大线程数量
int maximumPoolSize = 10;

// 定义保持活动时间的时长
long keepAliveTime = 60;

// 定义保持活动时间的时间单位
TimeUnit units = TimeUnit.SECONDS;

// 定义用于存储任务的阻塞队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

// 定义用于创建新线程的线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();

// 定义拒绝执行任务的处理程序
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

// 创建一个ThreadPoolExecutor并设置参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,       // 核心线程数量
    maximumPoolSize,    // 最大线程数量
    keepAliveTime,      // 保持活动时间
    units,              // 保持活动时间的时间单位
    workQueue,          // 存储任务的队列
    threadFactory,      // 线程工厂
    handler             // 拒绝执行任务的处理程序
);

在这个构造函数中,你可以指定核心线程数、最大线程数、非核心线程的空闲存活时间、时间单位、工作队列、线程工厂和拒绝策略

3. 使用Executors工厂类创建定时任务线程池

如果你需要创建一个专门用于执行定时任务和周期性任务的线程池,可以使用Executors工厂类中的newScheduledThreadPool方法:

int scheduledThreadCount = 5;
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(scheduledThreadCount);

注意事项

  • 线程池大小:线程池的大小应该根据程序的需求和系统资源来决定。过多的线程可能会导致资源竞争和上下文切换,而过少的线程则可能无法充分利用系统资源。
  • 任务提交:向线程池提交任务通常使用execute(Runnable command)submit(Callable<T> task)方法。execute用于提交无需返回结果的任务,而submit用于提交可能返回结果的任务。
  • 线程池管理:创建线程池后,你需要管理线程池的状态,如调用shutdown()shutdownNow()方法来关闭线程池。同时,可以通过isShutdown()isTerminated()等方法来检查线程池的状态。
  • 拒绝策略:当工作队列满了且创建的线程数达到最大线程数时,新提交的任务将被拒绝执行。你可以通过自定义RejectedExecutionHandler来处理这种情况。

通过以上方法,你可以根据实际需求灵活地在Java中创建和管理线程池。

6.使用线程池是否会引入新的并发问题,比如死锁、线程饥饿等,应该如何避免这些问题?(面试题)

使用线程池可以提高程序的性能和效率,但也可能引入新的并发问题,比如死锁和线程饥饿。为了避免这些问题,可以采取以下措施:

  1. 避免死锁:死锁是由于线程之间相互等待对方释放资源而无法继续执行的情况。为了避免死锁,可以避免使用多个线程池或者在使用多个线程池时保证资源的顺序分配,避免循环等待的情况

  2. 避免线程饥饿:线程饥饿是指某些线程长时间无法获取到资源而无法执行的情况。为了避免线程饥饿,可以设置合适的线程池大小,避免线程过多导致资源争夺,同时可以使用合适的线程调度算法来确保每个线程都能够得到执行的机会

  3. 使用合适的同步机制:在使用线程池时,需要注意合适的同步机制,避免多个线程同时访问共享资源导致数据不一致的情况。可以使用锁、信号量等同步机制来保证线程安全。

  4. 监控和调优:定期监控线程池的运行情况,及时发现并发问题并进行调优。可以通过监控线程池的线程数量、任务执行时间等指标来判断线程池的性能和效率,及时调整线程池的参数。

通过以上措施,可以有效避免线程池引入的新的并发问题,确保程序的稳定性和性能。

7.线程池的工作流程 ?

线程池的工作流程如下:

  1. 初始化线程池:线程池在启动时会创建一定数量的线程,并将它们保存在一个线程池中。

  2. 提交任务:当有任务需要执行时,可以将任务提交给线程池。

  3. 任务队列:线程池会将提交的任务放入一个任务队列中,等待线程池中的线程来执行。

  4. 线程执行任务:线程池中的线程会从任务队列中获取任务,并执行任务。

  5. 线程复用:执行完任务后,线程不会立即销毁,而是会等待新的任务。这样可以避免频繁地创建和销毁线程,提高效率。

  6. 线程池管理:线程池会根据需要动态调整线程数量,以确保任务能够及时地得到处理。

  7. 关闭线程池:当不再需要线程池时,可以调用关闭方法来关闭线程池,释放资源

8.如何在项目中使用线程池?有哪些线程池的最佳实践和注意事项

在项目中使用线程池是非常常见的做法,可以提高系统的性能和资源利用率。以下是使用线程池的最佳实践和注意事项:

  1. 创建线程池时,根据任务的性质和系统的负载情况来选择合适的线程池类型,如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等。

  2. 合理设置线程池的大小,包括核心线程数、最大线程数、任务队列的容量等参数,避免线程数量过多或过少导致性能问题。

  3. 使用线程池执行长时间的任务时,可以考虑使用定时务线程池ScheduledThreadPool,避免阻塞主线程。

  4. 对于IO密集型任务,可以使用CachedThreadPool自动调整线程数量,提高系统的并发性能。

  5. 对于CPU密集型任务,可以使用FixedThreadPool,固定线程数量,避免线程频繁创建和销毁的开销。

  6. 使用线程池时,及时处理异常,避免异常导致线程池中的线程泄漏或资源浪费。

  7. 在任务执行完毕后,及时释放资源,包括线程、内存等,避免资源的长时间占用和浪费。

  8. 使用线程池时,注意避免死锁和线程安全问题,合理设计任务的执行逻辑和同步机制。

  9. 监控线程池的运行情况,包括线程数量、任务队列的长度、任务执行时间等指标,及时调整线程池的参数。

  10. 在项目中使用线程池时,建议封装线程池的创建和使用逻辑,提高代码的可维护性和复用性。

遵循以上最佳实践和注意事项,可以有效地在项目中使用线程池,提高系统的性能和稳定性

9.线程池中的线程都有哪些状态(考察线程都有哪些状态)

  1. 新建状态(NEW):当线程被创建但还未启动时处于新建状态。
  2. 运行状态(RUNNABLE):线程正在运行或者等待CPU时间片。
  3. 阻塞状态(BLOCKED):线程被阻塞,通常是因为等待某个资源或者锁。
  4. 等待状态(WAITING):线程进入等待状态,等待其他线程通知或者唤醒。
  5. 限时等待状态(TIMED_WAITING):线程进入限时等待状态,等待一定时间后自动唤醒。
  6. 终止状态(TERMINATED):线程执行完毕或者发生异常导致线程终止。

 线程池中的线程会在这些状态之间切换,根据不同的任务和线程池的配置来进行管理和调度。

10.如何获取线程池的状态信息(如线程数、任务队列大小等)?

获取线程池的状态信息,可以使用Java的ThreadPoolExecutor类提供的一些方法来获取线程池的状态信息,例如:

  1. 使用getPoolSize()方法获取当前线程池中的线程数。
  2. 使用getActiveCount()方法获取当前正在执行任务的线程数。
  3. 使用getQueue()方法获取任务队列,然后可以通过size()方法获取任务队列的大小。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

int poolSize = executor.getPoolSize();
int activeCount = executor.getActiveCount();
int queueSize = executor.getQueue().size();

System.out.println("当前线程池中的线程数:" + poolSize);
System.out.println("当前正在执行任务的线程数:" + activeCount);
System.out.println("任务队列的大小:" + queueSize);

11.如何正确关闭线程池以防止资源泄漏?shutdown()shutdownNow()方法的区别是什么?

正确关闭线程池以防止资源泄漏通常需要调用线程池的shutdown()方法。这个方法会平缓地关闭线程池,等待所有任务执行完成后再关闭。在调用shutdown()方法后,线程池不再接受新的任务,但会等待已提交的任务执行完成。

shutdownNow()方法也可以关闭线程池,但它会尝试立即关闭线程池,不等待任务执行完成。这可能会导致一些任务无法执行完成,因此不推荐在正常情况下使用。

shutdown()和shutdownNow()方法的区别在于关闭线程池的方式,一个是平缓关闭,一个是立即关闭。通常情况下,建议使用shutdown()方法来关闭线程池,以确保所有任务都能执行完成

12.如何结合Future和Callable接口实现有返回值的任务?

要结合Future和Callable接口实现有返回值的任务,可以按照以下步骤进行:

  1. 创建一个实现Callable接口的类,该类的call()方法用于执行任务并返回结果。例如:
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 执行任务并返回结果
        return 10;
    }
}

 2.创建一个ExecutorService实例,用于提交Callable任务并获取Future对象。例如:

ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(new MyCallable());

 3.使用Future对象的get()方法获取任务执行的结果。例如:

try {
    Integer result = future.get();
    System.out.println("任务执行结果:" + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

 通过以上步骤,就可以结合Future和Callable接口实现有返回值的任务。在实际应用中,可以根据需要对Callable任务进行参数传递和异常处理等操作。

13. 对于定时任务,如何使用ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutorExecutorService的一个实现类,用于执行定时任务。要使用ScheduledThreadPoolExecutor执行定时任务,可以按照以下步骤进行:

1.创建ScheduledThreadPoolExecutor实例,指定线程池大小。例如

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

2. 使用schedule()方法执行定时任务。该方法接受一个Runnable任务和延迟时间作为参数,表示在延迟时间之后执行任务。例如:

executor.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("定时任务执行");
    }
}, 5, TimeUnit.SECONDS);
3.使用scheduleAtFixedRate()方法执行固定速率的定时任务。该方法接受一个Runnable任务、初始延迟时间和执行间隔时间作为参数,表示在初始延迟时间之后以固定速率执行任务。例如:
executor.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("固定速率定时任务执行");
    }
}, 0, 1, TimeUnit.SECONDS);

4.使用scheduleWithFixedDelay()方法执行固定延迟的定时任务。该方法接受一个Runnable任务、初始延迟时间和执行间隔时间作为参数,表示在每次任务执行完成后延迟固定时间再次执行任务。例如:

executor.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        System.out.println("固定延迟定时任务执行");
    }
}, 0, 1, TimeUnit.SECONDS);

 以上就是所有的内容了,上述这些,都是日常工作,还是做项目,或者是面试中都是十分常见的,建议收藏,重点掌握.感谢你的阅读,希望你一天愉快



网站公告

今日签到

点亮在社区的每一天
去签到