JavaEE:多线程代码案例(线程池)

36 篇文章 0 订阅

线程池

"池"这种思想,本质上就是能提高程序的效率.

最初引入线程池,就是因为进程太重了,频繁创建销毁进程,开销比较大~

"大"和"小"是相对的,随着业务上对于性能要求越来越高,对应的,线程创建/销毁的频次越来越多,此时,线程创建/销毁的开销就变得比较明显,无法忽略不计了~

线程池就是解决上述问题的常见方案.

什么是线程池?

线程池,就是把线程提前从系统中申请好,放到一个地方.后面需要使用线程的时候,直接从这个地方来取,而不是从系统重新申请. 线程用完了之后,也是还回到刚才这个地方.

为什么线程池更高效?

为啥我们说,从线程池里取线程,比从系统申请,来的更高效呢?
这就不得不说到 内核态 & 用户态 了.

内核态和用户态都是操作系统中的概念.
执行的很多代码逻辑,都是要用户态的代码和内核态的代码一起配合完成的.

操作系统 = 操作系统内核 + 操作系统配套的应用程序.

操作系统内核是操作系统的核心功能部分,它负责完成一个操作系统的核心工作.(管理)

应用程序有很多,这些应用程序,都是由内核统一负责管理和服务的,内核里的工作就可能是非常繁忙的 => 提交给内核要做的任务可能是不可控的.

举个例子:
比如,你想要办理取款业务,就需要来到柜台前,把你的诉求告诉柜员,让人家给你进行操作.

但是你忘记带身份证复印件了,现在你有两种办法:

  1. 去银行的大厅里的"自助复印机"自行复印.(整个过程连贯,可控,效率比较高)
  2. 柜员也可以帮我复印,但是可能要稍等一会.(整个过程不可控,效率比较低)

在这里插入图片描述

从系统创建线程,就相当于让银行的人给我复印.
这样的逻辑就是调用系统api,由系统内核执行一系列的逻辑来完成这个过程.

直接从线程池里取,这就相当于是自助复印,整个过程都是纯用户态代码,都是咱们自己控制的,整个过程更可控,效率更高.

因此,通常认为,纯用户态操作比经过内核的操作,效率更高~
最后总结一下线程池的优点:

  1. 降低资源消耗:减少线程的创建和销毁带来的性能开销。
  2. 提高响应速度:当任务来时可以直接使用,不用等待线程创建
  3. 可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。

Java中的线程池

Java标准库,也提供了现成的线程池,让我们直接使用.
先看看标准库的线程池~

标准库提供了类,ThreadPoolExecutor (构造方法,有很多参数)

在这里插入图片描述
让我们慢慢介绍:
1.
在这里插入图片描述
这个线程池,可以支持"线程扩容".
某个线程池,初始情况下,可能有M个线程.
实际使用中,发现M不太够用,就会自动增加M的个数.

在Java标准库中的线程池中,就把里面的线程分成两类:

  1. 核心线程(也可以理解成最少有多少个线程
  2. 非核心线程(线程扩容的过程中,新增加的)

核心线程数 + 非核心线程数的最大值 = 最大线程数

核心线程: 会始终存在于线程池内部
非核心线程: 在繁忙的时候被创建出来,不繁忙了,就会把这些线程真正释放掉.

2.
在这里插入图片描述
3.在这里插入图片描述
这个阻塞队列来描述当前要执行的任务有哪些.

此处的队列,我们可以自行指定,比如说:

  1. 队列的容量
  2. 队列的类型

4.
在这里插入图片描述

到这里就又疑问了,构造方法有什么"坑"?
让我来给大家解释一下.
首先我们知道,构造方法这是一个类里面的特殊方法,它必须和类名一样,多个版本的构造方法必须要使用"重载(overload)".

接下来,我们来看这样一个例子.
我们需要表示平面上的一个点,此时有两种方法,一种是记录下横坐标纵坐标,另一种是通过极坐标来表示.
在这里插入图片描述
写成代码就是这样

class Point {
	public Point(double x,double y) {...}
	public Point(double r,double a) {...}
}

此时,发现这两个方法无法构成重载.
使用构造方法创建实例,就会存在上述局限性.

为了解决上述问题,于是引入了"工厂设计模式".
通过"普通方法"(通常是静态方法)来完成对象构造和初始化的操作.

class Point {

}

class PointFactory {
	public static Point makePointByXY(double x,double y) {
		Point P;
		p.setX(x);
		p.setY(y);
		return p;
	}
	public static Point makePointByRA(double r,double a) {
		Point P;
		p.setX(r);
		p.setY(a);
		return p;
	}
}

以上就是一个简单的工厂设计模式的写法.
此处用来创建对象的static方法,就称为"工厂方法".
有时候,工厂方法也会放到单独的类里实现.
用来放工厂方法的类,称为"工厂类".

5.
在这里插入图片描述
这是上述所有参数中,最重要,也是最复杂的.

如果线程池的任务队列满了,还是要继续给这个队列添加任务,那该咋办呢?

当队列满了,不要阻塞,而是要明确的拒绝.

Java标准库中给出了以下四种不同的拒绝策略.
在这里插入图片描述
ThreadPoolExecutor 它的功能很强大,但是使用麻烦.

于是标准库对这个类进一步封装了一下,Executors 提供了一些工厂方法,可以更方便的构造出线程池
在这里插入图片描述
在这里插入图片描述
代码示例:

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

public class Demo19 {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 100; i++) {
//            service.submit(()->{
//
//            });
//      也可以写成:
//            service.submit(new Runnable() {
//                @Override
//                public void run() {
//
//                }
//            });
            int id = i;
            service.submit(()->{
                Thread current = Thread.currentThread();
                System.out.println("hello thread" + id + ", " + current.getName());
            });
        }
    }
}

运行结果:

在这里插入图片描述
可以看到执行这个代码,虽然100个任务都执行完毕了,但是,整个进程并没有结束,这是为什么呢?
答:此处线程池创建出来的线程,默认都是"前台线程",虽然main线程结束了,但是这些线程池里的前台线程仍然是存在的.

那要怎么结束呢?

可以在main方法的最后加上:

service.shutdown();

来把线程池里所有的线程都终止掉~
但是最好不要立刻就终止,可能任务还没执行完呢,线程就被终止了.

线程池使用示例

package other;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Demo1 {

    /**
     * 使用ThreadPoolExecutor创建一个忽略最新任务的线程池,创建规则:
     * 1.核心线程数为5
     * 2.最大线程数为10
     * 3.任务队列为100
     * 4.拒绝策略为忽略最新任务
     *
     */

    public static void main(String[] args) throws InterruptedException {
        // 依题意创建线程池
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, // 核心线程数
                10, // 最大线程数
                3, // 线程空闲时长
                TimeUnit.SECONDS, // 线程空闲时长的时间单位
                new LinkedBlockingQueue<>(100), // 任务队列
                new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略为忽略最新任务
        for (int i = 0; i < 2000; i++) {
            poolExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "已执行.");
            });
        }
        poolExecutor.shutdown();// 关闭线程池
}

在这里插入图片描述

线程个数指定多少合适?

我们在使用线程池的时候,需要指定线程个数.
线程个数,如何指定?指定多少合适?
比如,网上存在这样的结论:
假设当前机器cpu核心数是N(逻辑核心的个数)
网上给出的线程池个数,N,N-1,N+1,2N,2N-1…

这些说法并不是很准确

  1. 一台主机上,并不是只运行你这一个程序
  2. 你写的这个程序,也不是100%的每个线程都能跑满cpu,线程工作过程中,可能会涉及到一些IO操作/阻塞操作主动放弃cpu

    如果线程代码里都是算数运算,确实能跑满cpu,如果是包含了sleep,wait,加锁,网络通信,读写硬盘… 这些都会使线程主动放弃cpu一会

实际开发中,更建议的做法是通过"实验"的方式,找到一个合适的线程池的个数的值.
通过给线程池设置不同的线程数,分别进行性能测试,关注相应时间/消耗的资源指标,挑选一个比较合适的数值.

具体的可以看看这篇文章:
别再纠结线程池池大小、线程数量了,哪有什么固定公式 | 京东云技术团队

自己写一个简单的线程池

以下是固定线程数目的线程池

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

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

    // 此处的n表示创建几个线程
    public MyThreadPool(int n) {
        // 先创建出n个线程
        for (int i = 0; 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 void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class Demo20 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            int id = i;
            pool.submit(() -> {
                System.out.println("执行任务" + id + ", " + Thread.currentThread().getName());
            });
        }
    }
}

当前线程池,核心代码就写到这里,更多的功能,支持更多的参数,以及扩容/拒绝策略…写起来比较麻烦.

本文到这里就结束啦~

在这里插入图片描述

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月临水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值