线程池
"池"这种思想,本质上就是能提高程序的效率.
最初引入线程池,就是因为进程太重了,频繁创建销毁进程,开销比较大~
"大"和"小"是相对的,随着业务上对于性能要求越来越高,对应的,线程创建/销毁的频次越来越多,此时,线程创建/销毁的开销就变得比较明显,无法忽略不计了~
线程池就是解决上述问题的常见方案.
什么是线程池?
线程池,就是把线程提前从系统中申请好,放到一个地方.后面需要使用线程的时候,直接从这个地方来取,而不是从系统重新申请. 线程用完了之后,也是还回到刚才这个地方.
为什么线程池更高效?
为啥我们说,从线程池里取线程,比从系统申请,来的更高效呢?
这就不得不说到 内核态 & 用户态 了.
内核态和用户态都是操作系统中的概念.
执行的很多代码逻辑,都是要用户态的代码和内核态的代码一起配合完成的.
操作系统 = 操作系统内核 + 操作系统配套的应用程序.
操作系统内核是操作系统的核心功能部分,它负责完成一个操作系统的核心工作.(管理)
应用程序有很多,这些应用程序,都是由内核统一负责管理和服务的,内核里的工作就可能是非常繁忙的 => 提交给内核要做的任务可能是不可控的.
举个例子:
比如,你想要办理取款业务,就需要来到柜台前,把你的诉求告诉柜员,让人家给你进行操作.
但是你忘记带身份证复印件了,现在你有两种办法:
- 去银行的大厅里的"自助复印机"自行复印.(整个过程连贯,可控,效率比较高)
- 柜员也可以帮我复印,但是可能要稍等一会.(整个过程不可控,效率比较低)
从系统创建线程,就相当于让银行的人给我复印.
这样的逻辑就是调用系统api,由系统内核执行一系列的逻辑来完成这个过程.
直接从线程池里取,这就相当于是自助复印,整个过程都是纯用户态代码,都是咱们自己控制的,整个过程更可控,效率更高.
因此,通常认为,纯用户态操作比经过内核的操作,效率更高~
最后总结一下线程池的优点:
- 降低资源消耗:减少线程的创建和销毁带来的性能开销。
- 提高响应速度:当任务来时可以直接使用,不用等待线程创建
- 可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。
Java中的线程池
Java标准库,也提供了现成的线程池,让我们直接使用.
先看看标准库的线程池~
标准库提供了类,ThreadPoolExecutor (构造方法,有很多参数)
让我们慢慢介绍:
1.
这个线程池,可以支持"线程扩容".
某个线程池,初始情况下,可能有M个线程.
实际使用中,发现M不太够用,就会自动增加M的个数.
在Java标准库中的线程池中,就把里面的线程分成两类:
- 核心线程(也可以理解成最少有多少个线程
- 非核心线程(线程扩容的过程中,新增加的)
核心线程数 + 非核心线程数的最大值 = 最大线程数
核心线程: 会始终存在于线程池内部
非核心线程: 在繁忙的时候被创建出来,不繁忙了,就会把这些线程真正释放掉.
2.
3.
这个阻塞队列来描述当前要执行的任务有哪些.
此处的队列,我们可以自行指定,比如说:
- 队列的容量
- 队列的类型
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…
这些说法并不是很准确
- 一台主机上,并不是只运行你这一个程序
- 你写的这个程序,也不是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());
});
}
}
}
当前线程池,核心代码就写到这里,更多的功能,支持更多的参数,以及扩容/拒绝策略…写起来比较麻烦.
本文到这里就结束啦~