一、核心原理
1)引入
我们之前写的多线程的代码是这样的:当我们需要一个线程的时候,我们就可以创建线程的对象,代码跑完,线程就会消失。这种方式其实是不对的,它会浪费操作系统的资源。
所以我们也需要改进:准备一个容器用来存放线程,这个容器就叫做:线程池(ThreadPool)。
刚开始线程池里面是空的,没有线程。
当我们给线程池提交一个任务的时候,线程池本身它就会自动的去创建一个线程,拿着这个线程去执行任务,执行完了再把线程还回去。
第二次再提交任务的时候,它就不需要再去创建新的线程了,而是拿着已经存在的线程去执行任务,执行完了再还回去。
这个就是我们多线程的核心原理。
2)特殊情况
如果我们在提交第二个任务的时候,线程还正在执行第一个任务,它还没有还回去,此时线程池就会创建一个新的线程。
拿着新的线程去执行第二个任务,那么在这个时候,我又提交了很多其他的任务,此时线程池就会继续创建新的线程,执行新提交的任务。
任务执行完毕,它会把线程再还给线程池。
说道这里有的同学会有疑问:线程池它没有上线的吗?
线程池其实是有上线的,并且这个上线我们可以自己设置,假设我现在设置了最大线程数量为3。
那么这三个线程就会去执行前面的三个任务,后面的两个任务只能先排队等着。
3)核心逻辑
接下来梳理一下线程池的核心逻辑。
1、创建一个池子,池子中是空的
2、提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子。
下回再次提交任务时,不需要创建新的线程,就不需要创建新的线程了,直接复用已经存在的线程即可。
3、但是如果提交任务时,池子中没有空闲线程,而且也无法创建新的线程,任务就会排队等待。
4)打码步骤
代码步骤其实也非常简单
1、创建线程池
2、提交任务
PS:当我们提交任务的时候,线程池的底层会去创建线程,或者去复用已经存在的线程,这些代码是不需要我们自己写的,是线程池的底层会去自动完成。我们要做的就是给它提交任务。
3、当所有的任务全部执行完毕,就可以关闭线程池
但是在我们实际开发中,线程池一般是不会关闭的。因为服务器是24小时运行的,服务器一旦不关闭,那就是随时随地都有可能有新的任务要执行,那么线程池也就不会关闭。
创建线程池在Java中有一个工具类:Executors
,通过这个工具栏,我们就可以去调用方法,返回不同类型的线程池对象。
我们可以创建一个没有上线的线程池,但是这种方法并不是真正没有意义的上线,它也是有上线的,是int类型的最大值。
这个绝对是够用了,因为它还没有创建那么多线程,电脑就会先崩溃掉。因此我们会将它认为是一个没有上线的线程池。
static ExecutorService newCachedThreadPool() 创建一个默认的线程池
再往下,还会有第二种方式,它可以创建一个有上线的线程池。
static ExecutorService newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
二、代码实现
1)创建没有上线的线程池
//1.获取线程池对象,newCachedThreadPool()可以获取一个没有上限的线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
提交任务的时候也是调用方法:submit()
其中,我们可以给它提供 Runnable的实现类
,还可以给它提供 Callable的实现类
因此此时我们需要写一个实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
测试类完整代码
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
//3.销毁线程池,一旦池子被摧毁后,它里面所有的线程也会消失
pool1.shutdown();
运行代码,可以看见线程是有自己的名字 pool-thread-1
,后面就是我们在 run()
中打印的序号。
在刚刚我们说了,线程池一般是不会销毁的,因此需要将第三步注释掉,再给第二步多提交几个任务。
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
//pool1.shutdown();
运行程序,可以看见编号为 1-5
,由此可见线程池开启了五个线程去执行我们的任务。
2)展示复用的效果
我们可以将任务写的简单一些,不要循环打印那么多次了,直接打印一句话就行了
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---");
}
}
回到测试类中多提交几个任务,并且每次提交任务之前,都让 main线程
先睡个1秒钟,目的就是为了让上一个任务赶紧执行完毕,把线程还回去。
public static void main(String[] args) throws InterruptedException {
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
//3.销毁线程池
//pool1.shutdown();
}
任务执行完毕,可以发现每次都是线程1
3)创建有上限的线程池
static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
测试代码
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
-----------------------------
public static void main(String[] args) throws InterruptedException {
//1.获取线程池对象
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
//pool1.shutdown();
}
运行完毕,不管怎么看,都只有 123
,这就表示了线程池中最多只能有三个线程。
以上是我们通过代码的运行结果进行的验证,还有一种验证方式:利用Debug进行验证
注意关注一下变量
pool size
:目前线程池中有多少线程,现在是0,那就表示线程池刚开始创建的时候它里面是空的,什么也没有。workQueue
:记录了你当前有多少任务在排序等待,目前size
是0,因此一开始的时候是没有认为在等的。
点击下一步,当我们将第一个任务提交上去后, poll size
变成 1
了,表示线程池中已经有一个线程了。
再点击两次下一步,线程池中里面就有三个了,此时看好了,池子里面有3个线程,后面的队伍还是0,还没有任务在排队。
接下来再下一步,池子的长度还是 3
,但是在外面已经有一个任务在排队等待了。
再点击下一个,可以看见有两个任务在排队了。