线程池主要核心原理
- 创建一个池子,池子中是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
线程池的创建
线程池的创建,主要使用 Executors
工具类中的两个方法,一个是 newCachedThreadPool
,一个是 newFixedThreadPool
,前者是用来创建一个无长度上限的线程池,后者是用来创建一个有长度上限的线程池,他们都会返回一个 ExecutorService
对象,具体如下:
ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
ExecutorService e2 = Executors.newFixedThreadPool(3); //创建一个最多可以容纳5个线程的线程池
向线程池中提交任务
接下来我们测试线程池是否真的能达到像它所说的那样的效果,提交任务常使用的方法是 submit
或 execute
方法,通常通过 execute(Runnable)
提交任务仅执行而不返回结果,而 submit(Runnable)
会返回 Future
,可用于获取执行状态或异常信息。
我们做如下测试:
public class Main {
public static void main(String[] args){
//创建线程
ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
//ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 在运行。");
}
};
e1.submit(r);
}
}
如上,线程已经在执行了,可以发现程序并没有结束,因此在后面我们要在线程池不需要使用的时候进行销毁,但一般线程池是不需要关闭的。
我们接下来多测试几个线程:
public class Main {
public static void main(String[] args){
//创建线程
ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
//ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 在运行。");
}
};
e1.submit(r);
e1.submit(r);
e1.submit(r);
e1.submit(r);
e1.submit(r);
}
}
如图,我们添加了 5
个任务。为了进一步测试,我们在添加人物之前进行一小段时间的睡眠,验证已经执行完的线程池里的线程对象能不能得到复用,如下:
package top.mygld.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws InterruptedException {
//创建线程
ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
//ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 在运行。");
}
};
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
}
}
如上,线程 1 执行完毕后,被多次复用。
接下来再测试一下 newFixedThreadPool
:
package top.mygld.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws InterruptedException {
//创建线程
//ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
ExecutorService e2 = Executors.newFixedThreadPool(3); //创建一个最多可以容纳5个线程的线程池
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 在运行。");
}
};
e2.submit(r);
e2.submit(r);
e2.submit(r);
e2.submit(r);
e2.submit(r);
}
}
非常完美,如上图,线程最多只能被使用 3
个。
线程池的销毁
已经没有用的线程池,只需要使用 shutdown
方法销毁即可,这里不再演示。
自定义线程池
除了使用 Executors
提供的工厂方法,我们也可以通过 ThreadPoolExecutor
来创建一个更加灵活、可控的线程池。它的构造方法如下:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的最大存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂,用于创建新线程
RejectedExecutionHandler handler // 拒绝策略
)
参数含义
- corePoolSize 核心线程数,线程池会始终保持的线程数量,即使这些线程处于空闲状态。
- maximumPoolSize 最大线程数,当任务过多时,线程池最多能扩展到的线程数量。
- keepAliveTime & unit
非核心线程(超过
corePoolSize
的部分)在空闲时的存活时间,超时后会被回收。 - workQueue
存放等待执行任务的阻塞队列。常见选择:
ArrayBlockingQueue
(有界队列,数组实现)LinkedBlockingQueue
(无界队列,链表实现)SynchronousQueue
(直接提交任务,不存储)
- threadFactory 定义线程的创建方式,比如设置线程名、是否为守护线程等。默认工厂即可。
- handler
当线程池和队列都满了时的拒绝策略:
AbortPolicy
(默认,抛异常)CallerRunsPolicy
(由调用者线程执行任务)DiscardPolicy
(直接丢弃任务)DiscardOldestPolicy
(丢弃队列中最老的任务)
示例:自定义线程池
package top.mygld.demo;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 自定义线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
10, TimeUnit.SECONDS, // 非核心线程空闲存活时间
new ArrayBlockingQueue<>(3), // 有界任务队列,最多存 3 个任务
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:直接抛异常
);
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " 在执行任务");
try {
Thread.sleep(2000); // 模拟耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 提交 10 个任务,观察线程池如何调度
for (int i = 1; i <= 10; i++) {
try {
pool.execute(task);
} catch (RejectedExecutionException e) {
System.out.println("任务 " + i + " 被拒绝了!");
}
}
pool.shutdown(); // 关闭线程池
}
}
执行流程说明
- 前两个任务会直接启动 核心线程 处理。
- 接下来的三个任务会放入 任务队列。
- 当队列满了之后,线程池会继续创建新线程(最多到
maximumPoolSize=5
)。 - 如果线程池和队列都满了,第 9、10 个任务会触发 拒绝策略,直接抛异常并提示被拒绝。
最大并行数
最大并行数的含义
- CPU 层面: 对于计算密集型任务,最大并行数通常受 CPU 核心数限制(包括超线程)。比如你的 CPU 有 8 核(16 线程),理论上同时执行 16 个计算任务是最优的。
- 线程池层面:
对于 Java 中的
ExecutorService
或ThreadPoolExecutor
,最大并行数就是线程池maximumPoolSize
的值。线程池会根据这个值控制同时活跃线程的数量。 - 操作系统限制: JVM 的线程数也受操作系统限制,通常线程数太多会导致内存溢出或调度开销过大。
获取最大并行数可以通过下述代码获取:
public class Main {
public static void main(String[] args) {
int i = Runtime.getRuntime().availableProcessors();
System.out.println(i);
}
}
线程池多大合适
CPU 密集型运算
最大并行数 + 1
I/O 密集型运算
面试八股文重点
- volatile
- JMM
- 悲观锁、乐观锁、CAS
- 原子性
- 并发工具类
这些内容是面试经常会提问的点,后期再做整理。