线程池的主要工作
- 线程池做的工作主要是控制运行的线程的数量。
- 处理过程中将任务放入队列,然后在线程创建之后启动这些任务。
- 如果线程数量超过了最大数量,超出数量的线程排队等待。
- 等待其它线程执行完毕之后,再从队列中取出任务来执行。
特点
线程复用
- 减低了资源的消耗。
- 通过复制利用已经创建的线程降低线程创建和销毁造成的资源消耗。
控制最大并发数
- 提高响应速度。
- 当任务到达时,任务可以不需要等到线程创建就可以立即执行。
管理线程
- 提高线程的可管理性。
- 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。
- 线程池可以进行统一的分配,调优和监控。
如何使用
- ExecutorService 接口继承了 Executor 接口。
- 我们一般用 Executor 接口的池化对象来进行多态。
- 就像我们创建 ArrayList 来多态 List接口 而不是 Collection 接口。
架构说明
|
继承关系 |
编码实现
-
下边几种,测试线程池用用就行了。
-
一般自己用构造函数初始化一个线程池。
固定线程数
- 可控制线程最大并发数,超出的线程会在队列中等待。
/*
靠阻塞队列 LinkedBlockingQueue 来初始化
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Executors.newFixedThreadPool(int) // 执行长期的任务来说的话性能会好很多
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// ExecutorService 接口继承了 Executor 接口
// 我们一般用 Executor 接口的池化对象来进行多态
// 就像我们创建 ArrayList 来多态 List接口 而不是 Collection 接口
// 一池五个处理线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 模拟十个用户来办理业务
// 每个用户就是一个来自外部,来请求线程
try {
// 使用
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +
"线程\t 办理业务。。。");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭
threadPool.shutdown();
}
}
}
/*
pool-1-thread-1线程 办理业务。。。
pool-1-thread-4线程 办理业务。。。
pool-1-thread-1线程 办理业务。。。
pool-1-thread-3线程 办理业务。。。
pool-1-thread-2线程 办理业务。。。
pool-1-thread-3线程 办理业务。。。
pool-1-thread-1线程 办理业务。。。
pool-1-thread-4线程 办理业务。。。
pool-1-thread-5线程 办理业务。。。
pool-1-thread-2线程 办理业务。。。
*/
- 最多就五个线程在复用满足外部的十个业务请求。
一池一线程
- 创建一个单线程化的线程池。
- 它只会用唯一的工作线程来执行任务,保证所有任务按照指定的顺序执行。
/*
靠阻塞队列 LinkedBlockingQueue 来初始化
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
Executors.newSingleThreadExecutor() // 一个任务一个任务的执行场景,唯一的工作线程来执行任务,保证所有任务按照指定的顺序执行
package JUC.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zhaolimin
* @date 2021/11/17
* @apiNote 线程池测试
*/
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// 一个任务一个线程,一池一线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() +
"线程\t 办理业务。。。");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
/*
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
pool-1-thread-1线程 正在办理业务。。。
*/
- 我们可以看出这个线程池中只有一个线程在被使用。
一池多线程
- 创建一个可缓存线程池。
- 如果线程池长度超过处理的需要,可灵活收回空闲线程,若无可回收,则新建线程。
/*
靠阻塞队列 SynchronousQueue 来初始化
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
// 来了任务就创建线程运行,当线程空闲超过60s就给他销毁
new SynchronousQueue<Runnable>());
}
Executor.newCachedThreadPool() // 适用:执行很多短期异步的小程序或者负载较轻的服务器
public void autoThreadPool() {
// 一池多线程,数量会调整
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 16; i++) {
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "线程\t 办理业务。。。");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
/*
pool-1-thread-1线程 办理业务。。。
pool-1-thread-5线程 办理业务。。。
pool-1-thread-3线程 办理业务。。。
pool-1-thread-2线程 办理业务。。。
pool-1-thread-4线程 办理业务。。。
pool-1-thread-8线程 办理业务。。。
pool-1-thread-7线程 办理业务。。。
pool-1-thread-6线程 办理业务。。。
pool-1-thread-10线程 办理业务。。。
pool-1-thread-9线程 办理业务。。。
pool-1-thread-2线程 办理业务。。。
pool-1-thread-10线程 办理业务。。。
pool-1-thread-9线程 办理业务。。。
pool-1-thread-6线程 办理业务。。。
pool-1-thread-8线程 办理业务。。。
pool-1-thread-7线程 办理业务。。。
*/
- 线程池中的数量是随请求使用线程的次数来定的。
- 我的电脑最多一个线程池10个线程。
ThreadPoolExecutor类 (重点)
核心线程数
- 核心线程会一直存在,即使没有任务。
七大参数(重点)
// 线程池中的常驻核心线程数
// 当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中去
int corePoolSize
// 线程池能够容纳“同时执行的最大线程数”,此值必须大于1
int maximumPoolSize
// 多余的空余线程的存活时间
// 当线程池数量超过 corePoolSize 时,
// 同时当空闲时间达到 keepAliveTime 时,
// 多余的线程会被销毁,直到只剩下 corePoolSize 个线程为止
long keepAliveTime
// keepAliveTime的单位
TimeUnit unit
// 任务队列,被提交但是未被执行的任务,也就是阻塞队列
BlockingQueue<Runnable> workQueue
// 表示生成线程池中工作线程的线程工厂
// 用于创建线程,一般用默认的即可
ThreadFactory threadFactory
// 拒绝策略。表示当队列满了并且工作线程数大于等于线程池的最大线程数 maxmumPoolSize 时,
// 如何来拒绝任务
RejectedExecutionHandler handler
构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 非法参数检测
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
// 非法引用检测
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
//
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池工作原理(重要)
- 对应图中 1、2、3、4 四个步骤:
- 如果核心线程使用数量到了 corePoolSize,
- 那么新的任务先放阻塞队列里,
- 如果阻塞队列满了,那么就增加核心线程数,
- 此时增加的核心线程数不是指常驻的核心线程,是指临时的核心线程,
- 当增加的临时核心线程数等于 maximumPoolSize 的时候,就不再往线程池中添加新的临时线程。
- 如果此时阻塞队列和线程池线程数量都达到阈值,并且还源源不断的有新的请求,
- 此时就触发拒绝机制。
- 最后等到阻塞队列为空,线程池中没有线程有要处理的代码段,
- 此时我们判断临时核心线程的等待时间是否等于 keepAliveTime,如果等于,那就销毁临时核心线程。
- 最后我们处理完一切事物,就==只留下了空空的阻塞队列以及我们线程池的两个常驻核心线程==。
|
图示 |
拒绝策略(重要)
-
以下四个JDK内置的拒绝策略均实现了 RejectedExecutionHandler 接口。
-
AbortPolicy(默认)
-
直接抛出 RejectedExecutionException 异常。
-
阻止系统正常运行。
-
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
-
-
CallerRunsPolicy
- **“调用者运行”**机制,是一种调节机制。
- 该策略既不会抛弃任务,也不会抛出异常。
- 它会将某些任务回退给调用者,从而降低新任务的流量。
-
DiscardOldestPolicy
- 抛弃队列中等待最久的任务。
- 然后把当前任务加入到队列中尝试再次提交当前任务。
-
DiscardPolicy
- 直接丢弃该任务。
- 不处理,不抛异常,如果允许任务丢失的话,这是最好的一种方案。
自定义线程池案例
未使用自定义拒绝策略
AbortPolicy版
public class CustomThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,5
,60L, TimeUnit.MILLISECONDS
, new LinkedBlockingQueue<Runnable>(3)
, Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
try {
// 此时阻塞队列和线程池最大长度为 8,同时并发超过 8 的话就会造成拒绝
for (int i = 0; i < 9; i++) {
//for (int i = 0; i < 8; i++) { // 不会被拒绝
// 并发 9
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "线程\t 处理业务!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
/*
pool-1-thread-1线程 处理业务!
pool-1-thread-5线程 处理业务!
pool-1-thread-4线程 处理业务!
pool-1-thread-3线程 处理业务!
pool-1-thread-2线程 处理业务!
pool-1-thread-5线程 处理业务!
pool-1-thread-4线程 处理业务!
pool-1-thread-1线程 处理业务!
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3b9a45b3 rejected from java.util.concurrent.ThreadPoolExecutor@7699a589[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at JUC.threadpool.CustomThreadPoolDemo.main(CustomThreadPoolDemo.java:23)
*/
CallerRunsPolicy版
public class CustomThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,5
,60L, TimeUnit.MILLISECONDS
, new LinkedBlockingQueue<Runnable>(3)
, Executors.defaultThreadFactory()
,new ThreadPoolExecutor.CallerRunsPolicy());
try {
// 超出线程池允许最大并发度
for (int i = 0; i < 9; i++) {
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "线程\t 处理业务!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
/*
pool-1-thread-2线程 处理业务!
pool-1-thread-4线程 处理业务!
pool-1-thread-2线程 处理业务!
main线程 处理业务! // 没报异常也没报错,把业务回退给他的母线程运行,此时这个线程的母线程为main线程
pool-1-thread-1线程 处理业务!
pool-1-thread-3线程 处理业务!
pool-1-thread-2线程 处理业务!
pool-1-thread-4线程 处理业务!
pool-1-thread-5线程 处理业务!
*/
- 其它俩个慎用。
使用自定义拒绝策略
如何合理的配置线程池线程数
CPU密集型
-
CPU密集的意思是:
- 该任务需要大量的运算,如果没有阻塞,CPU一直全速运行。
- CPU的密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)。
- 所以单核CPU就别想加速了,再怎么加速也只有一个核心,最快也就一个核心的算力。
-
先查看我们的CPU核心数,再进一步去设置 corePoolSize 的大小。
-
System.out.println(Runtime.getRuntime().availableProcessors());
-
一般我们的CPU密集型任务配置尽可能少的线程数量。
-
公式大概是:CPU核心数+1个线程的线程池。
IO密集型
-
即该任务需要大量的IO,即大量的阻塞。
-
单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
-
所以在IO密集型任务中使用多线程可以大大的加速程序的运行。
-
在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间,所以也能有加速效果。
-
因为IO密集型的任务线程不一定一直在执行任务。
-
则应该配置尽可能多的线程。
- 一、CPU核数 * 2
- 二、CPU核数 / (1 - 阻塞系数) (阻塞系数在0.8~0.9)
死锁
是什么?
- 是指两个或者两个以上的线程或进程在执行的过程中,因抢夺资源而造成的一种互相等待的现象。
- 造成这种现象后如果没有外力干涉那么就无法进行下去,这就是死锁。
- 如果系统资源充足,线程或进程的资源请求都能满足,死锁出现的可能性就会很低,而不是不会出现。
|
图示 |
造成原因
- 系统资源不足。
- 进程运行推进顺序不合适。
- 资源分配不当。
- 请求并持有资源,互斥,不可中断,循环等待。
编码
package JUC.deadlock;
import java.util.concurrent.TimeUnit;
/**
* @author zhaolimin
* @date 2021/11/18
* @apiNote 死锁测试
*/
class HoldThread implements Runnable{
private String lock1;
private String lock2;
public HoldThread(String lockA, String lockB) {
this.lock1 = lockA;
this.lock2 = lockB;
}
@Override
public void run() {
// 可重入的前提是同一个锁,所以这里不是可重入锁
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "线程\t 持有" + lock1 + "\t正在尝试获得" + lock2);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldThread(lockA,lockB),"A").start();
new Thread(new HoldThread(lockB,lockA),"B").start();
}
}
/*
A线程 持有lockA 正在尝试获得lockB
B线程 持有lockB 正在尝试获得lockA
...
*/
- 字符串是常量,所以说,两个线程抢的锁是相同的两把锁。
- 如果是别的引用的话,这里的锁就是4把不同的锁,那么例子就出错。
- 而且可能出现,A线程已经全部运行完了获得了两把锁,B线程一把锁都没获得的情况,也可能反过来。
定位分析
-
linux:
ps -ef|grep xxxx
-
windows:
- windows下的java程序运行,也有类似linux下的查看进程的ps命令,但是目前我们只看java。
- jps.exe = java ps 可以用
jps -l
jps -l
查看正在运行的java程序,找到可疑进程的PID。jstack PID
查看当前进程的异常栈,有没有报出死锁。
Java stack information for the threads listed above: =================================================== "B": at JUC.deadlock.HoldThread.run(DeadLockDemo.java:35) - waiting to lock <0x00000007160d5f48> (a java.lang.String) - locked <0x00000007160d5f80> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) "A": at JUC.deadlock.HoldThread.run(DeadLockDemo.java:35) - waiting to lock <0x00000007160d5f80> (a java.lang.String) - locked <0x00000007160d5f48> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock. D:\DevelopmentEnvironment\Git\GitRepository\javase-learning>
码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes
如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。
—————————————————————— 爱你们的 noblegasesgoo