第十八讲 线程池(极其重要的内容)
1 线程池的概念
池:什么是池?池子,装水的。
为什么要有池子:蓄水,方便用水。想用就用,不用就放在那里备用。
线程池:池子里放的是线程,一条条已经开好的线程。
具体的线程是Thread类型的对象。
线程池就是将一个个new好的Thread类型的对象放在某个地方备用。
对象放在哪里?堆内存中。
线程池就是:在堆中放一些已经new好的Thread类型的对象。供别人调用。
这些对象,用完就还回去。相当于一个工具箱,用完之后把工具还回箱子中。
以前我们用线程的时候,哪里用哪里new,这样不是很好,不便于管理。
线程池就是提前准备好了线程,你用的时候只要去池子里面取就行了。
也就是说:
我要用老虎钳子,以前的方式,要用的时候,我就买一把,用完丢掉。
有了池的概念之后,就是我建了一个工具箱,箱子里面放了一把钳子
每次用的时候我去箱子里面拿,用完还回到箱子中去。这样就不浪费了
这种方式看上去是不是更加美好了。
优点:一次创建多次复用;便于管理;控制最大的并发数。
1. 降低资源消耗
2. 响应速度更快
3. 线程可管理了,可以统一监管。
2 java中线程池是怎么实现的
juc包中 Executor接口:执行器
Executor:
An object that executes submitted Runnable tasks
线程池是干什么的?存放线程的。线程是干什么的?处理并发任务的
有多个Runnable任务,去线程池中拿线程来处理。
有了线程池我们不再需要new Thread(new RunnableImpl()).start();
看看Executor的架构:

找到了线程池,ThreadPoolExecutor 这是一个类。
怎样构造线程池呢?
public class PoolTest {
public static void main(String[] args) {
ExecutorService poolExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int tem = i + 1;
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()
+ "\t 正在执行!" + tem);
});
}
}
}
以上代码会有什么问题:线程一直执行,不结束。
原因:我们从池子中拿到这唯一的一根线程,让他执行任务,任务执行完毕
但是没有还回去,就会认为这跟线程还有任务要执行,所以程序不会结束
线程池中的线程用完以后要关闭。
public class PoolTest {
public static void main(String[] args) {
ExecutorService poolExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int tem = i + 1;
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()
+ "\t 正在执行!" + tem);
});
}
poolExecutor.shutdown();
}
}
上述代码还有什么问题吗?
public class PoolTest {
public static void main(String[] args) {
ExecutorService poolExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int tem = i + 1;
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()
+ "\t 正在执行!" + tem);
});
Object object = null;
object.hashCode();
}
poolExecutor.shutdown();
}
}
Exception in thread "main" java.lang.NullPointerException
at com.tj.ThreadPool.PoolTest.main(PoolTest.java:24)
pool-1-thread-1 正在执行!1
程序仍然不会结束。
因为出现异常以后,后续的代码不会被执行,但是线程没有被释放
所以程序不会结束。
老师,出现空指针以后,main方法结束了。为什么这里不结束?
因为这里开启了另外一个线程,也就是有了另外一个栈空间
main方法确实结束了,主线程结束,栈销毁,但是线程池中的线程
正在执行,它所在的栈空间没有收到销毁的指令shutdown()
还回线程的本质是让它所在的栈空间销毁。
怎么解决上述问题:
以前,我们使用到锁的时候,trylock,因为,锁用完以后要释放
以前,我们使用IO,打开流以后,要关闭。
我们所知道的所有的关于关闭的方法都应该在finally中被调用
因为,不管发生什么,我都要确保它被关闭。这样才能不占资源
才能让程序正确执行。
public class PoolTest {
public static void main(String[] args) {
ExecutorService poolExecutor = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
final int tem = i + 1;
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()
+ "\t 正在执行!" + tem);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
}
}
public class PoolTest {
public static void main(String[] args) {
ExecutorService poolExecutor = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
final int tem = i + 1;
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()
+ "\t 正在执行!" + tem);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
}
}
以上:如果别人问你,实际开发中使用哪种方式创建线程池,你如何回答?都不用!
怎样创建线程池呢?自己定义。自己创建。
阿里java开发手册:第15页
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
3 线程池的底层原理
线程池的核心:ThreadPoolExecutor类
1. public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
关于ThreadPoolExecutor类的构造方法参数:7个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
对于7个参数的解析:
int corePoolSize:核心池的大小,池子的大小怎么衡量?线程数
int maximumPoolSize:池子最大的大小:最多能容纳多少条线程
long keepAliveTime:保持活跃时间,谁活跃?线程
TimeUnit unit:时间单位
BlockingQueue<Runnable> workQueue:阻塞队列,这个队列放什么玩意?
ThreadFactory threadFactory:线程工厂,干什么的?生产线程
RejectedExecutionHandler handler:拒绝策略
以上七个参数,告诉了我们整个故事的真相:
前两个参数:
int corePoolSize:主要干活的线程有多少条?也就是一直在线的线程数。
int maximumPoolSize:池子最多能容纳的线程数
通过这两个参数我们可以想到这样一个故事:
线程池它创建出来之后,并不是把所有的线程都激活,
一出来就干活的线程有corePoolSize个;
如果有更多的任务进来,corePoolSize个线程忙不过来的时候,
就会激活(maximumPoolSize-corePoolSize)个线程,让他们来帮忙干活。
第三四个参数:
long keepAliveTime:线程活跃。谁?core以外的线程。
一旦被激活,完事之后,会有一个活跃时间。然后就会还回去。
TimeUnit unit:时间单位
第五个参数:
BlockingQueue<Runnable> workQueue:阻塞队列
为什么要有阻塞队列?阻塞什么东西?阻塞队列中放的是什么?
线程池放的什么?线程
放线程干什么?处理并发任务。
BlockingQueue<Runnable>:这个是一个容器,放什么?并发任务。
他有容量吗?有
第六个参数:
ThreadFactory threadFactory:线程工厂,这个工厂生产线程。
这个工厂一次性生产maximumPoolSize个线程。
工厂中有个容器放线程。具体使用哪条创建好的线程,直接去工厂拿就行了。
工厂就是那个池子。
但是线程池(ThreadPoolExecutor)不是仅仅只是池子,
还有线程和并发任务以及处理机制。
第七个参数:
RejectedExecutionHandler handler:拒绝执行处理器。
什么意思?如果,容量爆了,还要接任务吗?
阻塞队列是不是一边添加任务,一边有线程来拿任务处理。
当这个队列满了,而且线程都忙不过来的时候,这时候不能再接了。
那么就会有处理机制来拒绝这些任务。
举个例子:
学校的校车车站,你们常常看到有几辆车在那里等着?2辆车(core)
实际学校的校车最多有5辆。(max)
这些校车都有校车工厂生产。factory
日常坐车人数不多,2辆车就够了。
当新生入学第一天,要去车站接人,
来的人太多了,2辆车不够用了。
但是,我不知道接下来来多少人,
我要把我最大的承载能力拿出来,所以5辆车都会开出
这时候,剩下的3辆车都要出动。
当开学第二天,人数不多了,2辆车够了,
那么增加的3辆车等了一个上午,发现还是没事干。
这三辆车从车站驶离,回去休息。
如果,五辆车都在车站忙不过来,到来的人应该在学校划定的区域排队等待。
先来先上车,后来后上车,这就是阻塞队列。
如果,队列都满了,而且车也在路上,还有源源不断的人来。
怎么办呢?你们不要坐校车,自己想办法去。坐公交、打的、走路。
这时候,等待区有工作人员竖起了牌子,请后来的同学和家长,自行坐车到校门口去
这就是拒绝机制。RejectedExecutionHandler。
1)学校不处理就乱套了,抛异常
2)自行处理,自己走路、自己打车、自己坐公交
3)学校帮忙找了公交车、的士,交给别人去处理...
线程吃的执行逻辑


4 自定义线程池
public class DefineThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 5, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
for (int i = 1; i <= 100; i++) {
final int tem = i;
pool.execute(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行!" + tem);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}
5 怎样确定线程池大小是合适的呢
根据任务的类型来的,任务一般分为两种:
IO密集型:IO次数较多
CPU密集型:主要占用的CPU的资源
如果是CPU密集型,一般线程池的大小为CPU核数或者是核数+1 +2
如果是IO密集型,大小一般是核数/阻塞系数。
怎样获得CPU核数?
int r = Runtime.getRuntime().availableProcessors();