【JavaEE】多线程案例-线程池

简介: 【JavaEE】多线程案例-线程池

95ce59710ee540c2b41f6d2ffb494fcb.gif

1. 什么是线程池

线程池是一种多线程处理形式,它处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。线程池避免了在处理短时间任务时创建与销毁线程的代价,从而提高了程序的效率和性能。

2. 为什么要使用线程池(线程池有什么优点)

我们都知道,在 Java 中使用多进程效率是比较低的,因为进程的创建和销毁的开销是比较大的,这样就会导致进程的创建和销毁的速度比较慢。所以在多进程的基础上就出现了线程。线程的创建和销毁都比较轻量,多个线程共用一套资源,这就避免了多次向计算机申请资源,极大提高了代码的执行速度。但是如果一个线程多次创建和销毁的话,也会导致系统资源的频繁调用,并且创建和销毁线程的而操作是内核态的,计算机通过调用相关的 API,然后进行线程的创建和销毁,但是既然是内核态操作,那么在计算机创建和销毁线程的过程中可能不是只干了这一件事,可能还会顺便帮其他线程提供资源等,这样就降低了代码的执行速度,所以为了解决线程多次创建和销毁,并且保证线程的创建和销毁属于用户态的操作的问题,就出现了线程池这一概念。在线程池中会提前创建 n 个线程,这些线程在执行完后不会销毁,而是继续存储在线程池当中等待下一次调用,正是因为线程池的这一概念,就使得线程创建和销毁的频率降低了。

总结来说,线程池的优点有以下这些:

  1. 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,任务无需等待线程创建,可以立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配,调优和监控。
  4. 避免系统过度切换:如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者产生“过度切换”的问题。

3. 如何使用Java标准库提供的线程池

3.1 创建一个线程池对象

在Java的 java.util.concurrent 包中提供了线程池相关的方法。那么如何创建出能执行线程池相关操作的对象呢?

ExecutorService service = Executors.newScheduledThreadPool(3);


712788d1b0df4509a81194bc31f9d36e.png

ExecutorService 是Java中的一个接口,它继承了Executor接口。

ExecutorService 接口在 java.util.concurrent 包中,它用于管理线程池。它提供了一种方式来管理和控制线程的生命周期。具体来说,它用于创建和管理线程池,可以执行线程,也可以关闭线程池。

而 Executors 则是一个工厂类,用来创建不同类型的 ThreadPoolExecutor 实例。

看到工厂类就需要提到一个常用的模式——工厂模式了,那么什么又是工厂模式呢?

3.2 什么是工厂模式

工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式,通过将对象的实例化过程封装在工厂类中,使得创建对象的方式更加灵活和可扩展。

在工厂模式中,客户端代码只需关注接口,而无需关注对象的具体创建过程。工厂模式通过提供一个统一的接口来创建不同类型的对象,这个接口定义了创建对象的标准方式。

3.3 为什么要使用工厂模式

工厂模式的作用是用来创建一个类的不同类型对象,既然这样的话,我们在一个类中使用多种重载的构造方法不就好了吗,为什么要多此一举再创建一个工厂类来创建一个类的不同类型的对象呢?

如果我们不使用工厂类来创建不同类型的对象,那么在创建对象的时候就需要在客户端中显式地选择合适的构造方法并提供对应的参数,这样的话类的具体创建逻辑就暴露了。而使用工厂模式的话,客户端代码只需调用工厂类的接口即可,而无需了解具体的创建逻辑。这样可以将对象的创建与使用代码分离,使得系统更加灵活,可扩展性更强。同时,使用工厂模式还可以避免在客户端代码中暴露对象的创建逻辑,提高了系统的安全性。

当创建线程池对象的时候,我们只需要调用 Executors 工厂类的对应静态方法,并且传递对应的参数就可以得到不同类型的 ThreadPoolExexutor 实例了,通过这个工厂模式既实现了创建一个类的不同实例的功能,又保证了系统的安全性。

3.4 Executors 创建不同具有不同特性的线程池

在知道什么是工厂模式之后,我们就利用这个工厂类来创建出需要的线程池实例,那么 Executors 工厂类又提供了哪些创建线程池对象的方法呢?它们又分别具有什么特性呢?

newFixedThreadPool() 方法

newCachedThreadPool() 方法

newScheduledThreadPool() 方法

Executors 工厂类还有很多不同的创建线程池对象的方法,这里我就不给大家一一展示出来了,大家如果感兴趣的话,可以去Java帮助文章上去查看。

3.5 ThreadPool 类的构造方法

通过查看源码,我们可以知道,Executors 工厂类创建的线程池对象都是通过传递不同的参数来实例化 ThreadPool 类的,也就是说 ThreadPool 类具有多种构成重载的构造方法,那么来看看这些不同的构造函数的参数分别代表什么吧。

  • corePoolSize 表示线程池中的核心线程数
  • maximumPoolSize 表示线程池中可含有的最大线程数
  • keepAliveTime 表示当线程池中的线程数量超过核心线程数(corePoolSize)时,多余的空闲线程在终止之前等待新任务的最长时间。
  • TimeUnit unit 用于指定keepAliveTime参数的时间单位。
  • workQueue 表示阻塞队列,可以根据需要设置阻塞队列的类型,如果需要优先级,则可以使用PriorityBlockingQueue;如果不需要优先级且任务的数目是恒定的,则可以使用ArrayBlockingQueue;如果任务的数目不是恒定的,则可以使用LinkedBlockingQueue
  • ThreadFactory 表示工厂类
  • RejectedExecutionHandle handle 表示拒绝策略

这里解决策略是面试中容易考的高频考点,那么这里我们就来详细的说说关于线程池的拒绝策略。

3.6 线程池的拒绝策略

当线程池中容纳的任务数量到达了最大限制之后,如果继续往里面添加任务的话,会出现什么情况呢?Java 中提供了4种拒绝策略。

  1. ThreadPoolExecutor.AbortPolicy 抛出异常
  2. ThreadPoolExecutor.CallerRunsPolicy 新添加的任务由添加任务的线程执行该任务
  3. ThreadPoolExecutor.DiscardOldestPolicy 丢弃掉最旧的未被处理的请求
  4. ThreadPoolExecutor.DiscardPolicy 丢弃掉当前新加的任务

3.7 调用 submit 方法添加任务

当创建了适当的线程池对象并且了解了其中创建的细节了之后,我们就需要调用该线程对象的相关方法来执行代码。

使用 submit 方法来添任务。

public class Demo1 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1");
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2");
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程3");
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程3");
            }
        });
    }
}

4. 自己实现一个线程池

同样的虽然 Java 标准库提供了线程池,但是我们作为初学者如果能够自己实现一个线程池,那么对于我们理解其中的逻辑和细节很有帮助。

class MyThreadPool {
  //创建一个阻塞队列
    BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
  //实现submit方法
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
  //实现构造方法,类创建的时候就会执行任务
    public MyThreadPool(int n) {
        for(int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                Runnable runnable = null;
                try {
                    runnable = queue.take();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                runnable.run();
            });
            t.start();
        }
    }
}

测试

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for(int i = 0; i < 4; i++) {
            int id = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行线程 " + id);
                }
            });
        }
    }
}


由于使用的是阻塞队列,所以当线程池中的任务达到数量限制的时候,如果再添加任务,会进入阻塞等待状态,这是不同于Java标准库提供的四种拒绝策略。


相关文章
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
201 0
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
152 2
|
4月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
9月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
452 60
【Java并发】【线程池】带你从0-1入门线程池
|
5月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
378 5
|
7月前
|
Java
线程池是什么?线程池在实际工作中的应用
总的来说,线程池是一种有效的多线程处理方式,它可以提高系统的性能和稳定性。在实际工作中,我们需要根据任务的特性和系统的硬件能力来合理设置线程池的大小,以达到最佳的效果。
235 18
|
9月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
353 20
|
9月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
1月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
149 6
|
4月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
306 83

热门文章

最新文章