线程是宝贵的内存资源,单个线程约占1MB空间,过多分配容易造成内存溢出
频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降
使用线程池,可设定线程分配的数量上限,将预先创建的线程对象存入池中,并重用线程池中的线程对象,避免频繁的创建和销毁
Java四种线程池
- 单个线程的线程池:newSingleThreadExecutor()
- 固定线程数量的线程池:newFixedThreadPool()
- 动态线程池:newCachedThreadPool()
- 调度线程池:newScheduledThreadPool()
测试:newSingleThreadExecutor()、newFixedThreadPool()、newCachedThreadPool()
package com.robot.juc.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 张宝旭
*/
public class ExecutorsTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
private int ticket = 100;
@Override
public void run() {
synchronized (this) {
ticket--;
System.out.println(Thread.currentThread().getName() + " 买了第:" + (100 - ticket) + " 张票");
}
}
};
// 创建单个线程的线程池
// ExecutorService executor = Executors.newSingleThreadExecutor();
// 创建固定线程数量的线程池
// ExecutorService executor = Executors.newFixedThreadPool(4);
// 创建动态线程池
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++) {
// 执行任务
executor.submit(runnable);
}
// 关闭线程池
executor.shutdown();
}
}
测试:newScheduledThreadPool()
- 不能在下面直接关闭线程池,可以判断指定条件,然后关闭
1、延时执行
例如:5秒后开始执行
executor.schedule(runnable, 5, TimeUnit.SECONDS);
2、周期执行,固定频率(如果执行中断了一段时间,则下次开始时会一次性把前面空闲的那段时间要执行的任务都补回来)
例如:0秒后开始执行,每隔1秒执行一次
executor.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS);
3、周期执行,固定延时(如果前面执行中断一段时间,不会把前面没执行的任务补回来)
例如:0秒后开始执行,每隔1秒执行一次
executor.scheduleWithFixedDelay(runnable, 0, 1, TimeUnit.SECONDS);
测试代码
package com.robot.juc.pool;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author 张宝旭
*/
public class ScheduleTest {
public static void main(String[] args) {
// 创建线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable runnable = new Runnable() {
private int count;
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println(Thread.currentThread().getName() + " ... " + sdf.format(new Date()));
count++;
// 当执行5次时,休眠一会
if (count == 5) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当执行20次时,关闭线程池
} else if (count == 20) {
executor.shutdown();
}
}
};
// 延时执行,5秒后开始执行
// executor.schedule(runnable, 5, TimeUnit.SECONDS);
// 周期执行,固定频率,0秒后开始执行,每隔1秒执行一次
// (如果执行中断了一段时间,则下次开始时会一次性把前面空闲的那段时间要执行的任务都补回来)
// executor.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS);
// 周期执行,固定延时,0秒后开始执行,每隔1秒执行一次
// (不会把前面的任务补回来)
executor.scheduleWithFixedDelay(runnable, 0, 1, TimeUnit.SECONDS);
}
}
ThreadPoolExecutor()
ThreadPoolExecutor的使用
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
// 创建线程池
// 核心线程数:2,最大线程数:3,存活时间1,单位:分钟,
// 阻塞队列:LinkedBlockingDeque 大小为2,线程工厂:默认工厂,拒绝策略:丢弃任务,抛出异常
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>(2), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i = 0; i < 5; i++) {
pool.submit(() -> System.out.println(Thread.currentThread().getName() + " ... "));
}
pool.shutdown();
}
}
七大参数
- 核心线程数:可执行的线程数,当任务量超出核心线程数时,会将任务添加到阻塞队列中
- 最大线程数:最大可使用的线程,当阻塞队列中也满了的时候,就会再次创建除核心线程之外的线程,但不超过最大线程数
- 存活时间:在最大线程数内,超出核心线程数的那些线程的存活时间
- 存活时间的单位:例如 TimeUnit.MINUTES 为分钟
- 阻塞队列:当执行任务超出核心线程数时,会将任务存入队列,如果超出队列,则会创建新的线程(在最大线程数内)
- 线程工厂:创建线程的工厂,一般使用默认
- 拒绝策略:当使用了最大线程数时(所有可用的线程都用了),而且阻塞队列也满了,会执行此策略
四大拒绝策略
1、AbortPolicy:丢弃任务,抛出异常,默认的拒绝策略
- 如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
2、DiscardPolicy:丢弃任务,不抛出异常
- 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
3、DiscardOldestPolicy:丢弃阻塞队列中老任务,将新的任务添加进去
- 比拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
4、CallerRunsPolicy:由创建此线程的线程去执行此任务
- 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务
测试分析-四大拒绝策略
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for(int i = 0; i < 5; i++) {
pool.submit(() -> System.out.println(Thread.currentThread().getName() + " ... "));
}
pool.shutdown();
}
}
1、AbortPolicy:丢弃任务,抛出异常,默认的拒绝策略
条件:当最大线程数为3,队列大小为1,共5个任务时
执行结果
pool-1-thread-1 …
pool-1-thread-2 …
pool-1-thread-3 …
pool-1-thread-1 …
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3feba861 rejected from java.util.concurrent.ThreadPoolExecutor@5b480cf9[Running, pool size = 3, active threads = 2, queued tasks = 0, completed tasks = 2]
分析
最大的线程数是3,阻塞队列中可以存一个,最多就可接收4个任务,当有5个任务时,就会执行拒绝策略,此拒绝策略为丢弃任务,抛出异常
2、DiscardPolicy:丢弃任务,不抛出异常
条件:当最大线程数为3,队列大小为1,共5个任务时
执行结果
pool-1-thread-1 …
pool-1-thread-2 …
pool-1-thread-1 …
pool-1-thread-3 …
分析
有三个线程执行,共执行了4个任务,有一个任务被丢弃了,没有抛出异常
3、DiscardOldestPolicy:丢弃阻塞队列中老任务,将新的任务添加进去
条件:当最大线程数为3,队列大小为1,共5个任务时
执行结果
pool-1-thread-1 …
pool-1-thread-3 …
pool-1-thread-2 …
pool-1-thread-1 …
分析
有3线程执行,前2个任务使用核心线程执行,第3个任务超出了核心线程数,所以进入阻塞队列,第4个任务发现了阻塞队列满了,而当前线程数还没有超出最大线程数,所以又创建了一个线程执行,第5个任务发现,此时最大线程数已经达到最大值了,而阻塞队列也满了,所以执行拒绝策略:丢弃阻塞队列中老任务(也就是第3个任务),将新的任务(第5个任务)添加进去,最后等前面任务执行完,释放线程后,执行了阻塞队列中的任务,也就是任务5被执行了,所以共执行了4个任务,丢弃了一个(任务3)
4、CallerRunsPolicy:由创建此线程的线程去执行此任务
执行结果
main …
pool-1-thread-3 …
pool-1-thread-3 …
pool-1-thread-2 …
pool-1-thread-1 …
分析
因为是主线程创建的线程,所以当任务没有线程执行的时候,会由创建此线程的线程来执行,也就是由主线程来执行,主线程名为main
线程池不允许使用Executors去创建,而是通过ThreadPooLExecutor的方式
说明: Executors返回的线程池对象的弊端如下:
1、FixedThreadpool 和 singleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2、CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM