初始JavaEE篇——多线程(6):线程池

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页: 我要学编程程(ಥ_ಥ)-CSDN博客

所属专栏: JavaEE

到现在为止,我们已经学习了两个经典的多线程案例了:饿汉模式与懒汉模式、阻塞队列与生产者---消费者模型。想要详细了解的小伙伴,可以去看往期文章。现在我们来学习另外一个案例:线程池。

目录

线程池

Java标准库中的线程池

参数解析

使用线程池

模拟实现线程池


线程池

概念:线程池,简单理解就是 一块内存中存放着多个线程。

作用:高效的创建与销毁线程。

举个例子来理解:

在一个阳光明媚的下午,高小强想吃鱼了,于是就告诉老莫,我想吃鱼了,老莫也是心领神会地跑去买鱼,但是很不幸的是高小强家里离买鱼的地方很远,所以老莫每次都得跑很远的路去买回来,一次两次还好,主要是高小强老是想着吃鱼,所以老莫也很是辛苦,因此老莫便想了个法子:直接在高小强家附近搞了个鱼塘,这样每次高小强想要吃鱼了,就可以直接下塘抓鱼,这样老莫也就轻松了不少。

在上面的例子中,老莫是充当CPU与操作系统的角色,而高小强是用户,高小强吃鱼这件事就是一个线程。当用户频繁地去创建与删除线程时,就会影响操作系统、CPU的效率(老莫只能经常去买鱼,而很少有时间去干自己想干的事情)。因此老莫这里的操作是建了一个鱼塘,对应计算机中的操作就是创建一个线程池。

线程池的工作原理:当外界有任务时,线程池的所在的线程就会随机启动其中的一个线程去执行任务,当任务执行完成时,这个线程并不会被销毁,而是重新回到线程池中等待下一次任务来临时,等待被调度。这就省下了创建线程与销毁线程所消耗资源。这里的资源主要是指CPU从用户态变为核心态以及操作系统切换到内核区工作。

Java标准库中的线程池

我们主要是要知道然后去创建并使用Java标准库中的线程池。

提供的类是:ThreadPoolExecutor。

参数解析

上面是这个类的构造方法,从上到下参数的个数是增多的。我们是要清楚构造方法的全部参数的。

下面是对最详细版的构造方法的参数解释:

注意:

1、 只有当核心线程数全部在工作时,如果这时需要处理新的任务,才会去创建新的线程。就好比一个公司,当内部员工足以处理这些业务时,就没必要花钱请外包,但是当内部员工已经忙不过来时,这时候才会需要外包来干新的任务。

2、当公司的业务过了旺季,到了淡季,这时候公司内部的员工都可能是出于空闲的状态,那外部更加没事干,因此公司就会考虑和外包解除合同。

3、线程工厂,这里使用了一种设计模式:工厂模式,其与我们前面学习的单例模式是出于同一级别的。工厂模式主要弥补构造方法的缺陷。例如,现在有一个类是用来描述平面直角坐标系中的一个点,描述的方式有两种:1、使用 (x,y) 坐标的方式;2、使用极坐标(用三角函数来实现)的方式;

因此,解决这样的问题,我们就可以使用工厂模式,将构造方法改为使用静态的方法,这样最终就不用通过构造方法来实现了。

代码演示:

java 复制代码
class Point {
    private double x = 0;
    private double y = 0;

    // 1、使用(x,y)的方式
    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    // 2、使用极坐标的方式
    /*public Point(double r, double a) {
        this.x = r * Math.cos(a);
        this.y = r * Math.cos(a);
    }*/

    public static Point getInstanceByXY(double x, double y) {
        return new Point(x,y);
    }

    public static Point getInstanceByRA(double r, double a) {
        return new Point(r*Math.cos(a), r*Math.sin(a));
    }
}

4、拒绝策略:

对于这四种拒绝策略,1、3、4 应该是很好理解的,但对于第二种来说,可能有点模糊。第二种方式,是告诉需要执行任务的线程:这个任务,我现在没空,你自己去把这个任务完成吧。然后需要执行这个任务的线程,就会自己把这个任务执行完。

我们先来了解一下,任务是什么?通过前面的学习,我们已经知道了,线程就是轻量级进程,也就是一段需要执行的指令。任务同样也可以看作是一段需要执行的指令,并且任务所需要执行的代码都是用 Runnable给包装起来的。给线程池去执行的话,就是让线程池对象调用 submit 方法,然后把包含任务代码的Runnable给作为参数扔给 sumbit 去执行。这就是线程池执行任务的过程。

从上面的分析,我们也可以得出一个结论:交给线程池执行的任务,而让自己(需要执行该任务的线程)执行任务 的区别在于:线程池会利用多线程的方式去执行该任务,而自己只会去串行执行,这样就影响了程序最终的效率。而让自己去执行,其实就是底层让 Runnable 去调用 run 方法。

1)有小伙伴可能会对 3、4有疑惑:丢弃最新的任务和让需要执行该任务的线程自己去执行 的区别是不是前者根本就没有线程去执行,而后者是调用submit的线程(也就是需要执行该任务的线程)去执行。

2)也有小伙伴可能会遇到这种说法:4 是丢弃当前任务。这种说法也是正确的,这里最新的任务和当前的任务都是指需要被执行的任务。当前任务不就是需要被执行的任务嘛,最新的任务不也是需要被执行的任务嘛,对叭,细细品味一下。

使用线程池

上面就是对构造方法的参数的解析,下面我们就来使用一下这个线程池。由于原本的类参数过多,因此JVM又对其进行了部分封装,最终我们使用的类是 ExecutorSever 。

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个线程数目固定的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
         // 创建一个很大的线程池,最大线程的数目是Integer.MAX_VALUE
        // ExecutorService service = Executors.newCachedThreadPool();        

        for (int i = 0; i < 10; i++) {
            int id = i;
            // 创建任务
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello task"+id+"--->"+Thread.currentThread().getName());
                }
            };
            // 调用线程池中的线程执行任务
            service.submit(task);
            Thread.sleep(1000);
        }
    }
}

我们去运行就会发现,上面的任务执行完成后,也就是所有的代码全部执行完成后,进程没有停下,还在继续。这是因为 线程池中的线程是前台线程的。

我们也可以去看这两个类的源码:

注意:上述代码的打印语句中不能使用 i ,因为在匿名内部类中,访问外部类的局部变量,采用的是变量捕获的方式,而这个方式固定了我们访问的变量必须是 final修饰的或者是事实 final(和我们上面一样,虽然没有用 final 修饰,但是最终的值并没有发生变化),i 在创建之后,还进行了 i++ 的操作,使得其发生了变化。

模拟实现线程池

接下来,我们就来模拟实现一个简单的线程池。

要求:与我们使用的线程池的效果要大致一样。

思路:实现线程池主要是要实现其中的 submit 方法,与构造方法。

模拟实现代码:

java 复制代码
public class MyThreadPool {
    // 定义一个阻塞队列,来接收要处理的任务
    private final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
    public MyThreadPool() {
        this(10);
    }

    public MyThreadPool(int nThread) {
        // 分配好线程数
        for (int i = 0; i < nThread; i++) {
            // 创建线程
            Thread t = new Thread(()->{
                // 要执行的任务 ------> 工作队列中找
                try {
                    while (true) { // 执行完成之后,不能让这个线程销毁(run方法执行完,线程就销毁了)
                        Runnable task = queue.take(); // 为空,内部会阻塞等待
                        task.run(); // 执行任务
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

    public void submit(Runnable task){
        // 把任务给到submit,然后由其来执行
        try {
            queue.put(task); // 为满,内部会阻塞等待
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

测试代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 1、创建线程池
        MyThreadPool threadPool = new MyThreadPool(2);
        // 2、创建任务并执行
        for (int i = 0; i < 10; i++) {
            int id = i;
            // 2.1 创建任务
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello task"+id+"--->"+Thread.currentThread().getName());
                }
            };
            // 2.2 执行任务
            threadPool.submit(task);
        }
    }
}

注意:

1、我们手动创建的线程是属于前台线程。

2、

1)怎么样保证线程不会销毁(还在执行的过程中) ------ 设置为前台线程。

2)怎么样保证线程在处于空闲状态,且不会被销毁 ------ 只有线程处于 run方法中,那么线程就不会被销毁,即在run方法中搞一个死循环即可。并且当工作队列为空时,线程会阻塞等待在 take 方法。

那如果我们实在是想要线程池中的线程在执行完任务之后,就销毁呢?

在我们自己实现的线程池中,只需要把创建的线程设为后台线程即可,而在Java提供的类中,我们需要用到 shutdown ,这个方法是无脑关闭,即使是有没有执行完成的任务,也会关闭。因此面对这种情况,就需要用到另一个方法:awaitTermination,这个方法是用来阻塞关闭线程池的,当线程池中任务没有执行全部完时,便会去等待我们手动设置的超时时间,(在这个超时时间之内)这个方法便会阻塞关闭线程池的线程,因此,上面两个方法一般都是连在一起用的。而如果线程池中任务已经全部执行完毕,就不会发生阻塞等待的情况。简单来说,就是检查有没有全部执行完成,如果执行完成了,就直接往下走,如果还存在没有执行完的,就会阻塞等待这个超时时间段,当超过这个时间段了,即使还没有全部执行完,此时也会往下走。

代码演示:

java 复制代码
public class Test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                try {
                    Thread.sleep(500);
                    System.out.println("任务执行完成");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        try {
            // 当线程池中还有任务没有执行完,就会阻塞等待 3s;反之,则直接往下执行
            boolean terminated = executorService.awaitTermination(3000, TimeUnit.MILLISECONDS);
            if (terminated) { // 当没有触发阻塞等待,就会返回true;反之,则返回false
                System.out.println("所有的任务都已经执行完了");
            } else {
                System.out.println("存在部分任务没有执行完");
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }

        executorService.shutdown(); // 这个代码就是线程池中还存在没有执行的任务
    }
}

运行结果:

好啦!本期 初始JavaEE篇------多线程(6):线程池 的学习之旅到此结束啦!我们下一期再一起学习吧!

相关推荐
Tanecious.1 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐1 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
战族狼魂1 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
Tttian6222 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
xyliiiiiL2 小时前
ZGC初步了解
java·jvm·算法
杉之3 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch3 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
独好紫罗兰4 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法
天天向上杰4 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal
闪电麦坤955 小时前
C#:base 关键字
开发语言·c#