【Java 多案例分析】——初识线程池

2024-01-01 11:37:24

个人主页:兜里有颗棉花糖
欢迎 点赞👍 收藏? 留言? 加关注💓本文由 兜里有颗棉花糖 原创
收录于专栏【Java系列专栏】【JaveEE学习专栏
本专栏旨在分享学习JavaEE的一点学习心得,欢迎大家在评论区交流讨论💌

一、线程池概念

在一些场景中我们需要频繁的创建和销毁线程(这样的话就会有很大的成本开销),所以我们可以使用线程池提前创建好一些线程,当我们后续需要使用某个线程的时候,我们直接从线程池的池子里拿这个线程就可以,相当于从线程池中获取到现有的线程)的方式来节约频繁创建和销毁线程所带来的成本。

现在来看一个问题:为什么从线程池取线程要比从系统中创建线程更加高效呢?
如果从操作系统系统这里创建线程,需要调用系统api,然后进一步的由操作系统内核来完成线程的创建。由于操作系统是为所有的进程提供服务的,所以线程什么时候创建好其实是不可控的。
如果是从线程池这里获取线程的话(这种方式是可控的),上面创建线程的过程都已经操作好了,此时就是一个去线程的过程。简单来说,当我们从线程池获取线程时,实际上是从线程池中获取一个预先创建的空闲线程,而不是动态地创建新线程。

当然,Java中提供了现成的线程池来供我们使用。

二、线程池的创建和使用

如何创建线程池

  • ExecutorService service = Executors.newFixedThreadPool(4);

在这里插入图片描述
下面是创建线程池的四种方法:

  • Executors.newFixedThreadPool(4):这是创建固定线程数量的线程池
  • Executors.newCachedThreadPool():创建动态数目变化的线程池
  • Executors.newSingleThreadExecutor():创建包含单个线程的线程池(即只包含一个线程),这种创建线程的方式比原生创建线程的api更简单一些
  • Executors.newScheduledThreadPool():创建一个具备定时执行任务能力的线程池,它会在指定的时间间隔内周期性地执行任务。类似于定时器的效果,可以添加一些任务,这些任务会在后续的某个时候进行执行,这些任务在执行的时候并不是只有一个扫描线程在执行任务,而是可能由多个线程共同执行所有的任务(比如说这里有50个任务,那么这50个任务就会由这些扫描线程共同来完成执行这50个任务)。

上述4种方法创建的是4种不同类型的线程池,当然除了上面4种线程池外,Java标准库中还提供了一个接口更为丰富的线程池类ThreadPoolExecutor。这里解释一下,上面介绍的4种线程池其实是对原生类ThreadPoolExecutor进行了封装而得到的4种线程池,这4种线程池只是为了我们使用方便所以才对ThreadPoolExecutor类进行了封装。如果我们这四种线程池不能够很好的解决我们当下的问题的话我们就可以使用原生的线程池类ThreadPoolExecutor(因为此类可以提供我们更多可调整的选项来供我们使用以便更好的解决我们的实际需求)。

如何使用线程池

上面线程池对象创建好了之后,现在我们来使用线程池对象,我们可以通过submit()方法将任务添加到线程池中。

service.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hell world!!!");
    }
});

示例线程池代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo24 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        for(int i = 1;i <= 10;i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hell world!!!");
                }
            });
        }
    }
}

运行结果如下:
在这里插入图片描述
解释:创建一个固定大小为 4 的线程池,并向线程池中提交 10 个任务,任务是打印一句话 “hello world!!!”。
当任务执行完毕后,线程会自动返回到线程池中,此时线程池中依然有空闲线程,可以继续执行任务。

现在来总结一下如何使用线程池:

  • 先创建线程池对象示例。
  • 调用submit()方法来添加任务。

三、ThreadPoolExecutor类

我们现在来看一下这个类的构造方法,请看:
在这里插入图片描述

这里对上图种ThreadPoolExecutor类的最后一个构造函数,也就是参数最多最复杂的构造函数进行解释。
首先我们要知道ThreadPoolExecutor类中的线程个数是会根据当前任务的情况发生动态变化的。

  • corePoolSize:核心线程数,即线程池最少要有这些数量的线程数。
  • maximumPoolSize:最大线程数,即线程池中最多只能包含这些数量的线程数。
    以上两个参数可以保证在繁忙的时候可以高效的处理任务,也可以保证在空闲的时候不会浪费资源。
  • long keepAliveTime, TimeUnit unit:是用来设置线程池中空闲线程的存活时间的参数,前者表示时间的数值,后者表示时间的单位(比如毫秒或者秒)。当空闲线程在这个时间之内没有任务需要执行的话,这些空闲线程就会自动销毁。
  • BlockingQueue workQueue:线程池内有很多任务,这些任务可以使用阻塞队列进行管理。线程池可以内置阻塞队列,当然也可以手动指定一个阻塞队列。
  • ThreadFactory threadFactory:通过这个工厂类来按照不同的方式创建线程。
  • RejectedExecutionHandler handler:当线程池中的阻塞队列满了之后,此时继续添加任务,针对这个新添加的任务的应对方式或者执行策略,即此时我们可以手动指定应对策略来处理这个新添加的任务(在Java标准库中已经为我们提供好了现成的应对策略,如下:)。
  • 在这里插入图片描述
  • 应对策略1: ThreadPoolExecutor.AbortPolicy:这里针对新添加的任务会直接抛出异常,此时线程池停止执行任务(我们可以理解为罢工开摆)。
  • 应对策略2:ThreadPoolExecutor.CallerRunsPolicy:谁是添加这个任务的线程就由谁来执行这个任务(简单来说就是调用者去执行)。
  • 应对策略3:ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的任务来去执行新的任务。
  • 应对策略4:ThreadPoolExecutor.DiscardPolicy:这里会直接丢弃掉新添加的任务。

四、线程池的简单实现

代码如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 通过这个submit方法将任务添加到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // n表示线程池有几个线程
    public MyThreadPool(int n) {
        for(int i = 1;i <= n;i++) {
            Thread t = new Thread(() -> {
                while(true) {
                    try {
                        // 取出任务并执行
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
}
public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for(int i = 1;i <= 1000;i++) {
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " hell world");
                }
            });
        }
    }
}

运行结果如下:
在这里插入图片描述

补充

这里我们补充一点,线程池中的线程个数是如何设置的,具体应该设置为多少合适呢?

首先我们要只要不同的线程要完成的工作往往是不一样的。

  • 有的线程的工作属于CPU密集型:主要负责运算,即这种CPU密集型的工作大部分都是需要在CPU核心上去进行工作的,CPU需要给这些工作安排核心才可以让需要完成的工作有所进展。举例:如果CPU是N个核心的话,当线程数量为N的时候,理想状态下每个CPU核心都会被安排到一个线程,如果此时再想添加更多的线程的话,这些新添加的线程就会进行排队等待状态(即不会有新的进展),这种情况下我们再怎么增加线程也不会有新的进展了,即增加线程数量反而会让调度调度开销变大从而影响到线程的执行效率。所以CPU密集型的任务不应该让线程数超过CPU核心数
  • 有的线程的工作属于IO密集型:比如读写文件、用户输入、网络通信等涉及到大量的等待时间。等待过程中并没有使用CPU,这样的线程即使更多一些也不会给CPU造成太大的负担。所以,当线程属于IO密集型的时候,如果CPU核心数是16,此时如果我们设置的线程数量是32的话也是没有问题的(因此的大部分的线程都是在等待过程,甚至是CPU的占用率还很低)

当然在实际的开发过程中,一部分的线程工作是CPU密集型的,一部分的线程工作是IO密集型的。此时我们其实是很难去量化CPU密集型和IO密集型的比例的。换句话说一个线程几成是在CPU上运行,几成是在等待IO都是不确定的。我们最好是通过实验的方式来找到最合适的线程数

五、总结

上述的线程池分为两组:一组线程池是对ThreadPoolExecutor类进行的分装;另一组线程池是就是原生ThreadPoolExecutor类了。具体使用哪一组的哪一种线程池需要我们根据具体场景来进行选择。

关于线程池中线程的数量问题我们可以通过性能测试,即尝试不同的线程数目来找到性能和系统资源开销比较均衡的线程数量。

本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_74352571/article/details/135315245
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。