线程池是多线程编程中非常重要的概念之一,使用线程池可以大大提高线程的使用效率和系统的性能。线程池不仅可以避免频繁地创建和销毁线程带来的资源浪费和性能损耗,还可以控制线程的数量,避免线程过多导致系统负载过高而出现崩溃的情况。本文将深入讲解线程池的概念、原理和使用方法,以及如何根据实际需求选择合适的线程池实现
怎么说呢?什么是线程池呢?我举一个例子大家就应该明白了.
在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,
而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来
一个任务,起一个线程进行处理的模式。
很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知
道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着
业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快
递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带
出的线程池的模式。
线程池的好处,当然有很多,其实我们从计算机的视角去看待的话.从线程池取出线程,属于纯用户态操作.不涉及到和内核的操作.当然很多人或许很疑惑了,什么是用户态,什么又是内核态.我下面就会进行解释.
用户态是指应用程序执行的一种模式,它只能访问被操作系统允许的资源,例如CPU、内存、文件等。在用户态下,应用程序不允许直接访问系统底层的资源和硬件设备,所有的系统调用和中断都必须通过操作系统提供的API接口进行。
内核态是操作系统执行的一种模式,它可以访问操作系统的所有资源和硬件设备。在内核态下,操作系统拥有所有的系统资源,可以直接控制硬件设备、处理异常、调度进程等。内核态下的操作系统具有最高的权限,可以访问所有的系统资源。
举一个生活中的例子,就好比是在驾驶汽车时的操作。在平时的行驶中,我们只能使用车上的控制器来完成各种操作,例如加速、刹车、打方向等,这就相当于在用户态下操作。而当发生紧急情况或者需要进行特殊操作时,我们需要调用车上的安全系统或者专业的修理工进行处理,这就相当于在内核态下操作。在内核态下,我们可以直接对汽车的引擎、刹车等核心部件进行操作,具有更高的权限和更强的控制力。
在解释完什么是内核态和用户态的操作以后,大家就会明白为什么线程池的操作比较高效,因为它属于用户态操作.
当然我们java也提供了线程池的API,我们可以看一下具体的方法使用.
代码如下:
public class ThreadDemo28 {
public static void main(String[] args) {
ExecutorService pool=Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
关于对这段代码的解释如下:
首先,通过 Executors.newFixedThreadPool(10) 方法创建了一个固定大小为 10 的线程池 pool。
然后,通过 pool.submit() 方法将一个 Runnable 对象提交给线程池,表示要执行一个任务。
这个任务会在线程池中的某个线程上执行。在本例中,这个任务只是简单地输出了一个字符串 “hello”。
当然,我们java中创建线程池的方式不止这一种,这里列举出几种,大家可以参考一下.
我们会对TheadPoolExecutor这个对象进行解释,为什么会这么说呢?我们现来看看Executors的创建线程池的一个方法,如下:
其实这里面的线程池对象实际上是TheadPoolExecutor,所以我们要去看看,这里面的线程池对象的参数里面有什么.
我这里给出了这个对象的一些关系解释.
接下来让我们看看线程池里面究竟有什么.
我们给出一一解释:
corePoolSize:线程池的核心线程数,也就是线程池中保持运行的线程数量。当任务数大于核心线程数时,线程池会创建新的线程执行任务。如果线程池中的线程数大于核心线程数,且任务队列已满,则新提交的任务会被拒绝,并抛出RejectedExecutionException异常。如果使用无界队列,该参数就没什么意义了。
maximumPoolSize:线程池中最大的线程数。当任务队列已满且当前线程池中的线程数小于最大线程数时,线程池会创建新的线程执行任务。如果线程池中的线程数等于最大线程数时,新提交的任务会被拒绝,并抛出RejectedExecutionException异常。
keepAliveTime:线程空闲时间。当线程池中的线程数大于核心线程数时,多余的空闲线程在空闲时间达到keepAliveTime后会被回收。如果keepAliveTime为0,则表示空闲线程立即被回收。
unit:时间单位,用于指定keepAliveTime的时间单位。
workQueue:任务队列,用于存储等待执行的任务。当线程池中的线程数等于核心线程数时,新提交的任务会被存储在任务队列中等待执行。
threadFactory:线程工厂,用于创建新的线程。
handler:拒绝策略。当线程池中的线程数等于最大线程数且任务队列已满时,新提交的任务会被拒绝并交给拒绝策略处理。
当然如果看了这上面的解释,你可能觉得有点大,有点空.我们再来说一遍.
你把线程池当做一个公司,公里里面有正式工和实习生.
那么corePoolSize就是正式工(核心线程数)
maximumPoolSize就是正式工+实习生(最大线程数)
当然有时候会出现一种情况,正式员工签了劳动合同,不能被随便辞退,二实习生不需要劳动合同,那么随时就可以辞退.大家想象这个时候,公司有可能比较忙就让实习生来帮忙,增加生产力,不忙的时候,就把实习生裁掉,这样的情况是不是符合keepAliveTime呢?
另外我们对任务有时候会进行分配,怎么分配会很方便呢?是不是看任务单上的名单进行分配呢?线程池里要管理很多任务,这些任务也是通过阻塞队列来组织的!程序猿可以手动指定给线程池一个队列
此时程序猿就很方便的可以控制/获取队列中的信息了,submit方法其实就是把任务放到该队列中.
这就需要workQueue(任务队列)进行操作.
我们既然要实现一个线程池,我们就要列出一下,我们的具体步骤.
1.核心操作为 submit, 将任务加入线程池中
2. 实现固定线程数的线程池
3. 开始执行任务
大概的流程就是如上几个操作.
代码如下:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
//使用阻塞队列来存放任务
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
//将任务加入阻塞队列中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//此处实现固定线程数的线程池
public MyThreadPool(int n){
for (int i=0;i<n;i++){
Thread t=new Thread(()->{
while (true){
Runnable runnable= null;
try {
runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class ThreadDemo29 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool=new MyThreadPool(10);
for (int i=0;i<10;i++){
int number=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+number);
}
});
}
Thread.sleep(3000);
}
}