线程池
普通线程池的设计思想
在完成某一个功能实现的时候,需要产生多个任务协作来完成,如果每个任务都使用一个线程来解决,每次创建线程以及线程的上下文切换都是需要消耗资源的;尤其是不可能永无止境的创建新线程,因为cpu 的核心数目也是有限的;使用享元设计模式,使得创建出来有限的线程构成线程池,使得多个任务共享使用线程池,可以减少系统的开销;
基于线程池的运行流程大致为:
1、相关方法产生任务需要运行,将任务放置到任务队列中;
2、线程池中的有限线程拿到任务并且执行;
可以使用消费者模式对于任务的添加以及消耗进行理解;
Blocking Queue 是阻塞队列,可以平衡产生的任务(可以设置阻塞队列的长度);
在没有任务的时候,线程池里面的线程可以在 Blocking Queue 里面进行等待;当任务太多的时候,线程池里面的线程处理不了,此时也是可以放在 Blocking Queue 里面进行等待的;
JDK 里面的线程池实现
JDK 线程池状态
RUNNABLE 代表的是一个负数,所以在数字大小的比较中是最小的;
为什么线程状态信息,以及线程数量信息要保存在一个 int 中?
因为这样可以保证原子性,使用一次 cas 操作就可以操作赋值,不需要两次的 cas 操作,所以使用一个 int 更好一些;
ThreadPoolExecutor 构造方法 十分重要
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
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;
}
关于上面的ThreadPoolExecutor 构造方法的七个参数的注释,方便理解(来自参数的英语解释的翻译)
参数 | 含义 |
---|---|
corePoolSize | 保留在池中的线程数,即使它们是空闲的,除非设allowCoreThreadTimeOut |
maximumPoolSize | 线程池中的最大线程数 |
keepAliveTime | 救急线程在终止前等待新任务的最长时间 |
unit | keepAliveTime 参数的时间单位 |
workQueue | 保存没有执行的任务的队列 用于在执行任务之前保存任务的队列。此队列将仅保存由execute方法提交的Runnable任务 |
threadFactory | 执行器创建新线程时使用的工厂 |
handler | 由于达到线程边界和队列容量而阻塞执行时使用的处理程序 |
模拟线程池的运行步骤
下面的图示中 corePoolSize = 2;
maxmuismPoolSize = 3;
救急线程 = 3 - 2 = 1;(救急线程在任务阻塞队列满了之后才会启动使用,生命周期可以自己设计)
存在救急线程的执行流程:
1、线程池刚开始是没有线程的,当第一个任务提交给线程池之后,线程池会创建出来一个新线程来执行任务;
2、当线程数组达到 corePoolSize 并且没有线程空闲的时候,此时新的任务会进入 workQueue 里面进行排队等待,直到出现有空闲的线程;
3、队列选择了有界线程,任务超过了队列大小的时候,会创建 maximumPoolSize - corePoolSize 数目的线程来救急;
4、救急线程也使用结束之后,此时来了新任务,会执行拒绝策略;具体的拒绝策略 JDK 提供了四种选项:
5、当线程需要被使用的高峰过去之后,此时的超过 corePoolSize 的救急线程有一段时间没有任务可以做的时候,需要节省资源,使得救急线程释放,这个由 keepAliveTime 以及 unit 来控制实现;
对于不同的策略的解释查看上图中注解的部分;
Executors 工厂方法创建线程池
实际上就是调用不同的构造方法,进行参数的组合,实现自己的线程池的创建需要;
用来各种用途的线程池
newFixedThreadPool
由于任务队列是没有界限的,所以大量的任务来的时候,可能导致内存溢出的情况,禁止使用;
创建出来固定大小的线程池
救急线程数目 = 0 (nThread - nThread)
适合任务量是已知的,任务相对比较耗时的;
newCachedThreadPool
任务队列是没有界限的,所以禁止使用,可能造成内存溢出的情况;
带有缓冲功能的线程池;
没有核心线程,全部的线程都是救急线程,救急线程的生存实现是 60s
救急线程在 60s 的时候被回收;
救急线程可以被无限的创建;
队列没有容量,只有取的时候,才能放进去,不然放不进去任务;
没有取的线程是不会放进去任务的,全员外包;
newSingleThreadExecutor
只有一个线程,核心线程是 1 ;总线程是 1 ,没有救急线程;
应用场景:多个线程之间,排队执行;
创建的一个线程,每次执行一个任务,任务执行结束自己去无界队列中取任务即可;
单线程和单线程线程池之间的区别:
1、保证始终都能有一个线程是可以用的,自己创建出来的单个线程,这个线程奔溃之后,没有补救措施了;
2、单线程的线程池与其他的线程池定义的初始线程数量为 1 的区别遇到再仔细甄别;
3、newFixedThreadPool 里面参数设置为 1 和这个 newSingleThreadExecutor 单线程线程池之间的区别是:返回的内容不一致的;newSingleThreadExecutor 使用到了装饰器模式;
线程池中一组与提交任务相关的方法
execute(Runnable command)
@Slf4j(topic = "c.Test")
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
submit(Callable task)
与 execute 不同的是,Callable 是存在返回结果的;
@Slf4j(topic = "c.Test")
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
// 将线程的执行结果返回到线程池中
// 使用 future 进行返回参数的接收
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "ok";
}
});
// 主线程中获取线程池中的线程执行的返回结果
System.out.println("返回的参数 future: " + future.get());
}
}
上面的代码实现了 submit() 方法与 Callable 结合使用,来实现这种有返回结果的执行任务;
返回结果的接收使用 future ;
future 使用的是保护性暂停的模式接收另一个线程的返回结果;上面的代码表现为在主线程接收线程池中的线程的返回结果;
Callable 是一个单方法的接口;可以使用 lambda 进行替换;
invoke()
上面的invoke() 方法是接收任务的集合,执行所有的任务,返回一个 future 的集合;
下面的invoke() 在上面的执行基础上,添加了超时时间,在一定的时间中,传递进来的任务集合是没有完成掉的话,会将后面的任务取消执行;
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 上面创建了线程池,线程池里面存在固定个数的线程,可以用线程池对象调用执行任务的相关方法,目的就是让任务完成;
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
() -> {
log.debug("begin");
Thread.sleep(1000);
return "1";
}, () -> {
log.debug("begin");
Thread.sleep(1000);
return "2";
}, () -> {
log.debug("begin");
Thread.sleep(1000);
return "3";
}, () -> {
log.debug("begin");
Thread.sleep(1000);
return "4";
}, () -> {
log.debug("begin");
Thread.sleep(1000);
return "5";
}));
}
invokeAny()
返回最先得到的结果,有一个任务执行结束之后,其他的任务都不执行了;
传递进去一个任务集合,什么任务可以优先获取到线程的执行?按照集合的顺序得到?
后序探讨;
shutdown()
将线程池中的状态改变为:SHUTDOWN
不会接收新的任务;
已经提交的任务会执行结束;
此方法不会阻塞调用线程的执行,不会阻塞主线程的执行,想让主线程等可以执行其他的方法 awaitTermination()
;
比如主线程调用了 线程池的 shutsown ,不会等待线程池中的所有线程执行结束之后,主线程才会执行,主线程会直接执行自己下面的逻辑代码;
源代码如下:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态
advanceRunState(SHUTDOWN);
// 只会打断空闲线程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 尝试终结(没有运行的线程可以立即终结,存在运行的线程不会等待,正在运行的线程自己结束,自己等待即可,线程池的状态和那些正在运行的线程的状态是不一样的,线程池先结束了)
tryTerminate();
}
异步模式 - 工作线程
1、有限的工作线程,异步的处理无限多的任务,一个线程负责多个任务;
2、不同的任务类型使用不同的线程池,可以有效的避免饥饿,提升系统的效率;
饥饿现象的产生 - 线程池中的线程不足
饥饿现象的问题解决 - 使用不同类型的线程池
@Slf4j(topic = "c.TestDeadLock")
public class TestStarvation {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
创建合理的线程数量
任务调度线程池
有时候,希望任务是反复的执行,或者任务是延缓执行,可以使用 java.util.Timer 可以用来调度线程;但是这个线程只能是一个线程进行调度的,前一个任务的延迟执行是会影响到后面的任务的执行的;
Time 实现线程的延缓执行是比较脆弱的,使用起来也是不方便的;
使用任务调用功能的线程池
newScheduledThreadPool
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
pool.scheduleAtFixedRate(() ->{
System.out.println("running...");
},1,1,TimeUnit.SECONDS);
pool.schedule(()->{
System.out.println("延迟执行 1 秒");
},1, TimeUnit.SECONDS);
pool.schedule(()->{
System.out.println("延迟执行 2 秒");
},2, TimeUnit.SECONDS);
pool.schedule(()->{
System.out.println("延迟执行 5 秒");
},5, TimeUnit.SECONDS);
}
在线程池中捕捉异常
1、自己手写 try catch 进行日志的打印
2、使用 callable 创建线程,里面可以存在输出错误日志信息;
一个定时线程的应用场景
固定在每一周的特定时间中执行特定的线程;
public class ThreadFinate {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
// 当前的时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
// 计算当前下次一定的线程执行的时间
LocalDateTime time = now.withHour(12).minusMinutes(0).withSecond(0).withNano(0).with(DayOfWeek.MONDAY);
// 下一次的执行时间在本周可能已经过去了,在此处重新计算下一次的执行时间,是不是将时间加一周
if (now.compareTo(time) > 0) {
time = time.plusWeeks(1);
}
// 传递到 pool.scheduleAtFixedRate 方法里面的时间差
long innitialTime = Duration.between(now, time).toMillis();
pool.scheduleAtFixedRate(()->{
System.out.println("按照特定的时间执行这个线程");
}, innitialTime,1, TimeUnit.MILLISECONDS);
}
}
Tomcat 里面的线程池
Tomcat Connector 里面使用到的线程池 Executor 是线程池;
Tomcat 的线程池扩展了 Java 自带的 ThreadPoolExecutor ,处理的策略是稍微的有一些不同的;
一个更加高级的线程池 Fork/Join
使用
1、创建任务对象
2、让任务使用 Fork/Join 执行
public class TestForkJoin {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
// System.out.println(pool.invoke(new AddTask1(5)));
System.out.println(pool.invoke(new AddTask3(1, 1000000)));
}
}
@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {
int n;
public AddTask1(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + '}';
}
@Override
protected Integer compute() {
if (n == 1) {
log.debug("join() {}", n);
return n;
}
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
log.debug("fork() {} + {}", n, t1);
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}
@Slf4j(topic = "c.AddTask")
class AddTask2 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask2(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask2 t1 = new AddTask2(begin, mid - 1);
t1.fork();
AddTask2 t2 = new AddTask2(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} + {} = ?", mid, t1, t2);
int result = mid + t1.join() + t2.join();
log.debug("join() {} + {} + {} = {}", mid, t1, t2, result);
return result;
}
}
@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask3 t1 = new AddTask3(begin, mid);
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}