线程池
Java并发知识导图
由于线程池这一块涉及到Java并发的相关知识,但是Java并发的知识点异常多,特意做成一个详细的思维导图。
线程池概述
使用线程池工具类Executors
或者是ThreadPoolExecutor
来创建线程池。线程池使用以后不会被关闭,除非显示进行关闭。
线程池从功能上看,就是一个任务执行器。
对比线程池submit方法和execute方法
- submit 方法有返回值,用Future 封装
- execute 方法无返回值,类似于Runnable类中的run方法
- submit 方法抛异常可以在主线程中catch 到。
- execute 方法执行任务是捕捉不到异常的。
使用ThreadPoolExecutor
创建线程池
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20,
30, TimeUnit.SECONDS, workQueue, new CustomerThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
//拒绝策略:提交任务的线程自己去执行该任务
for (int i = 0; i < 10; ++i) {
executor.execute(new Task(i));
}
executorService.shutdown();//关闭线程池
使用Executors
工具类创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; ++i) {
executorService.execute(new Task(i));
}
executorService.shutdown();//关闭线程池
ThreadPoolExecutor创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//......
}
corePoolSize
:表示线程池保有的最小线程数。比如公司中的项目组,即使有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。maximumPoolSize
:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
参考自:《Java并发编程实战》:
【线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableprocessors来动态计算。
要设置线程池的大小只需要避免过大和过小这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。如果线程池过小,那么将导致许多空闲的处理器无法工作,从而降低吞吐率。
要想正确地设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU ?多大的内存?任务是计算密集型、IO密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源?如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池可以根据各自的工作负载来调整
对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费。)对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。这种估算不需要很精确,并且可以通过一些分析或监控工具来获得。你还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率的水平。】- 在Java中,可以通过Runtime来获取CPU的核心数
int N_CPUS = Runtime.getRuntime().availableProcessors();
keepAliveTime & unit
:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个"一段时间"的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于corePoolSize ,那么这个空闲的线程就要被回收了。workQueue
:工作队列,用于存储需要执行的任务。threadFactory
:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。ThreadFactory是一个接口,需要传入ThreadFactory的一个实现类。handler
:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。
ThreadPoolExecutor 已经提供了以下 4 种策略。
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
- Java 在 1.6 版本还增加了allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走
示例
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20,
30, TimeUnit.SECONDS, workQueue, new CustomerThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
//拒绝策略:提交任务的线程自己去执行该任务
for (int i = 0; i < 100; ++i) {
executor.execute(new Task(i));
}
}
static class Task implements Runnable {
private int task;
public Task(int task) {
this.task = task;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行任务: task" + task);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class CustomerThreadFactory implements ThreadFactory {
private AtomicInteger serial = new AtomicInteger(0);
@Override
public Thread newThread(Runnable task) {
Thread thread = new Thread(task);
//thread.setDaemon(true); //根据需要设置守护线程
thread.setName("CustomerThread-" + serial.incrementAndGet());
return thread;
}
}
}
执行结果部分截图。main线程中只负责提交任务,但是却发现main线程也执行了任务,说明是拒绝策略起了作用
ThreadPoolExecutor
的execute
方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//当工作线程数小于核心线程数的时候,增加工作线程的数量
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果任务能成功放入队列,我们依然需要再次检查是否需要增加一个新的线程(因为在上次检查之后已有的线程死掉了)
//如果线程池已经关闭了,那么就回滚,如果再次检查发现已经没有线程了则重新建立一个线程
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//当任务无法放入队列,也无法从新添加一个新的Thread的时候,就拒绝该任务
else if (!addWorker(command, false))
reject(command);
}
缓冲队列
BlockingQueue 是双缓冲队列。BlockingQueue 内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
- ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO 顺序排序的。
- LinkedBlockingQueue:大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue 有大小限制,不指定大小,其大小有Integer.MAX_VALUE 来决定。其所含的对象是FIFO 顺序排序的。
- PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator 决定。
- SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。
Executor
Executors创建线程池的方法
-
newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
-
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
ExecutorService executorService = Executors.newFixedThreadPool(16);
-
newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
ExecutorService executorService = Executors.newCachedThreadPool();
-
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(16);
package conc0302.threadpool;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class NewScheduledThreadExecutorDemo {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(16);
for (int i = 0; i < 100; i++) {
final int no = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("当前线程: " + Thread.currentThread().getName());
System.out.println("start:" + no);
Thread.sleep(1000L);
System.out.println("end:" + no);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 10s后执行
executorService.schedule(runnable, 10, TimeUnit.SECONDS);
}
executorService.shutdown();
System.out.println("Main Thread End!");
}
}