Bootstrap

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

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

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

所属专栏:JavaEE

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

目录

线程池

Java标准库中的线程池

参数解析

使用线程池

模拟实现线程池


线程池

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

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

举个例子来理解:

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

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

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

Java标准库中的线程池

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

提供的类是:ThreadPoolExecutor。

参数解析

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

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

注意:

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

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

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

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

代码演示:

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 。

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 方法,与构造方法。

模拟实现代码:

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);
        }
    }
}

测试代码:

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,这个方法是用来阻塞关闭线程池的,当线程池中任务没有执行全部完时,便会去等待我们手动设置的超时时间,(在这个超时时间之内)这个方法便会阻塞关闭线程池的线程,因此,上面两个方法一般都是连在一起用的。而如果线程池中任务已经全部执行完毕,就不会发生阻塞等待的情况。简单来说,就是检查有没有全部执行完成,如果执行完成了,就直接往下走,如果还存在没有执行完的,就会阻塞等待这个超时时间段,当超过这个时间段了,即使还没有全部执行完,此时也会往下走。

代码演示:

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):线程池 的学习之旅到此结束啦!我们下一期再一起学习吧!

;