1-1、周课介绍:
- 线程池的基本使用、特点、注意点
- ThreadLocal的基本使用、原理、注意事项
- 分布式基础、核心概念
- Docker的下载、安装和基本使用命令
- 独立制作Docker容器
- Nginx的安装、基本使用和常用命令
- 使用Nginx搭建文件服务
- 消息队列RabbitMQ的核心概念queue、message和exchange
- RabbitMQ的四种交换机模式
- SpringBoot整合RabbitMQ实例
初始化线程池——线程池,治理线程的法宝
- 线程池的自我介绍
- 新建和停止线程池
- 常见线程池的特点和用法
- 任务大多线程,如何拒绝
- 钩子方法,给线程池加点料
- 实现原理,源码分析
- 使用线程池的注意点
- 线程池的重要性:
- 什么是池:软件中的“池”,可以理解为计划经济,避免创建线程带来的大开销,可复用线程,可控制资源总量。
如果不使用线程池,每一个任务新开一个线程:
- 一个线程
EveryoneTaskOneThread.java
package threadpool;
public class EveryTaskOneThread {
public static void main(String[] args) {
Thread thread = new Thread(new Task());
thread.start();
}
static class Task implements Runnable{
@Override
public void run() {
System.out.println("执行力任务");
}
}
}
- for循环创建线程
FoeLoop.java
package threadpool;
public class ForLoop {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Task());
thread.start();
}
}
static class Task implements Runnable{
@Override
public void run() {
System.out.println("执行力任务");
}
}
}
- 当数量上升到1000
package threadpool;
public class ForLoop {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(new Task());
thread.start();
}
}
static class Task implements Runnable{
@Override
public void run() {
System.out.println("执行力任务");
}
}
}
这样开销大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题。问题一:反复创建线程开销大 问题二:过多的线程会占用太多内存
解决以上两种问题的思路:
- 用少量的线程——避免内存的过多使用
- 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗
- 加快响应速度
- 合理利用CPU和内存
- 统一管理资源
服务器接收大量请求时,使用线程池技术是非常合适的,可以大大减少线程的创建和销毁次数,提高服务器的工作效率
实际上,在开发过程中,如果需要创建5个以上的线程,那么就可以使用线程池来管理。
- 线程池构造方法的参数
- 线程池是该手动创建还是自动创建
- 线程池里的线程数量设置多少比较合适
- 停止线程池的正确方法
| 参数名 | 类型 | 含义 |
|---|---|---|
| corePoolSize | int | 核心线程数 |
| maxPoolSize | int | 最大线程数 |
| keepAliveTime | long | 保持存活时间 |
| workQueue | BlockingQueue | 任务存储队列 |
| threadFactory | ThreadFactory | 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程 |
| Handler | RejectedExecutionHandler | 由于线程池无法接受你所提交的任务的拒绝策略 |
- corePoolSize指的是核心线程数——线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池等待有任务到来时,再创建线程去执行任务
- 最大量maxPoolSize-在核心线程数的基础上,额外增加的线程数的上线。
- 如果线程数小于corePoolSize,创建一个新线程来运行任务。
- 如果线程数等于(或大于)corePoolSize但少于maximumPoolSize,则将任务放入队列。
- 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程。
- 如果队列已满,且线程数大于或等于maxPoolSize,则拒绝。
- corePoolSize
- workQueue
- maxPoolSize
- 比喻:烧烤店的桌子
举个例子:
- 线程池:核心池大小为5,最大池大小为10,队列为100.
- 因为线程中的请求最多会创建5个,然后任务将会被添加到队列中,直到达到100,当队列已满时,将创建最新的线程maxPoolSize,最多到10个线程,如果再来任务,就拒绝。
- 通过设置corePoolSize 和 maximumPoolSize 相同,就可以创建固定大小的线程池
- 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
- 通过设置maximumPoolSize为很高的值,就可以允许线程池容纳任意数量的并发任务。
- 只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize。
如果线程池当前的线程数多于corePoolSize,那么多余的线程空闲时间超过keepAliveime,他们就会被终止
ThreadFactory 用来创建线程- 默认使用Executors.defaultThreadFactory()
- 创建出来的线程都在同一个线程组
- 如果自己指定threadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等等
常见3种队列类型:
- 直接交接: SynchronousQueue
- 无界队列: linkedBlockingQueue
- 有界对列: ArrayBlochingQueue
作用:给用户线程提供服务
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) ,分类标准是线程是否
会阻止JVM的停止——只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部
继续工作;只有当最后一个非守护线程结束时,所有守护线程才会随着JVM一同结束工作。(非守
护线程等同于用户线程)
我们知道,Java虚拟机通常会继续执行线程,直到发生以下两种中的任一情况时,Java程序才能运
行结束:
1.已调用System.exit()方法
2.所有非守护程序线程的线程都已结束
而一般情况下我们不会调用System.exit()方法,所以大部分的Java程序的结束都是由于所有用户线
程都结束而导致的。
所以可以认为,任何一个守护线程都是整个JVM中所有用户线程(非守护线程)的管家。Daemon
的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它是一
个很称职的守护者。
守护线程的特性
线程类型默认继承自父线程
守护线程创建的线程为默认是守护线程,同样,用户线程创建的线程默认为用户线程。非守护线程
如果想创建一个守护线程,需要调用Thread.setDaemon来设置它(Thread类用布尔值daemon属性
来表示线程是否是守护线程),并且,该方法必须在start之前调用,否则会抛出 IllegalThreadState
Exception 异常。
被谁启动?
通常由JVM启动,而不是由用户去启动。当JVM启动时,通常会有一个非守护线程(通常为执行ma
in函数的线程)。
不影响JVM退出
当只剩下守护线程时,JVM就会退出,因为如果只剩下守护线程,就没必要继续运行程序了。
守护线程没结束并不会影响JVM的正常停止:假设所有用户线程都结束了,那么就算有5个守护线程
正在运行,JVM也会正常停止:
守护线程和普通线程的区别
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经
全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了,这是因为没有了“被守护
者”,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
这就是守护线程的作用:告诉JVM不需要等待它退出,当JVM中所有的线程都是守护线程的时候,JV
M就可以正常的退出了。
我们是否需要给线程设置为守护线程?
我们通常不应把自己的线程设置为守护线程,因为设置为守护线程是很危险的。比如线程正在访问
如文件、数据库的时候,所有用户线程都结束了,那么守护线程会在任何时候甚至在一个操作的中
间发生中断,所以守护线程永远不应该去访问固有资源。
代码案例:
定义三个线程类,分别是ThreadA,ThreadB,ThreadC。继承Thread类,重写run()方法,在方法
中循环输出线程名称以及执行次数。其中ThreadA,ThreadB为用户线程,循环输出5次。ThreadC
为守护线程,循环输出20次(次数太少无法观察到效果,可以增加循环次数,例如循环输出50次,
100次等)
- 手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险
- 自动创建线程池(即直接调用JDK封存好的方法)可能带来哪些风险?
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedTreadPoolSize {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
class Task implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName());
}
}
}
newFixedThreadPool
易造成大量内存占用,可能会导致OOM
调整一下,把内存调小一些,“Edit Configuration”->VM Options : -Xmx8m -Xms8m
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThredPoolSizeOOM {
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executorService.execute();
}
}
}
class SubThread implements Runnable{
@Override
public void run() {
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
newFixedThreadPool
当请求堆积时,可能会占用大量的内存
| Parameter | FixedThreadPool | cached | Scheduled | Single |
|---|---|---|---|---|
| corePoolSize | constructor_args | 0 | constructor-args | 1 |
| maxPoolSize | same as corePoolSize | Integer.MAX_VALUE | Integer.MAX_VALUE | 1 |
| keepAliveTime | 0 seconds | 60 seconds | 0 seconds | 0 seconds |



