线程池顾名思义是管理一组线程的池子。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程不会立即销毁,而是等待下一个任务。
方法一:通过ThreadPoolExecutor构造函数来创建线程池(推荐)。
public class TaskExecutor {
//线程池核心线程数量
public static final int CORE_POOL_SIZE = 10;
//线程池最大线程数
public static final int MAX_POOL_SIZE = 30;
//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
public static final int KEEP_ALIVE_TIME = 100;
//线程池等待队列
public static final int BLOCKING_QUEUE_SIZE = 10000;
//通过ThreadPoolExecutor构造函数来创建线程池
public static final ThreadPoolExecutor PROCESS_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.MICROSECONDS,
new LinkedBlockingDeque<Runnable>(BLOCKING_QUEUE_SIZE),
new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build());
public static void executor(Runnable task) {
PROCESS_EXECUTOR.execute(task);
}
}
方法二:通过Executor框架的工具类Executors来创建线程池
public class TaskExecuter {
public static void main(String[] args) {
//通过Executors工具类来创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
threadPool.execute(() -> {
System.out.println("Hello wys");
});
}
}
问题思考:
在实际的开发中为什么推荐使用通过ThreadPoolExecutor构造函数的方式来创建线程池,而不推荐使用Executors工具类的方式来创建线程池呢?
答:
通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,因为我们通过使用ThreadPoolExecutor构造函数的方式去创建线程的话,可以指定线程池的核心参数,如:线程池核心线程的数量、线程池最大线程的数量、空闲线程的存活时间、任务队列、拒绝策略等,这样便于对线程池更好的统一的去管理,避免了资源耗尽的风险。
另外,Executors 返回线程池对象的弊端如下:
我们通过查看ThreadPoolExecutor构造函数源码来了解下线程池核心参数
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;
}
ThreadPoolExecutor3个重要参数:
其他常见参数:
线程的饱和策略handler:如果当前同时允许的线程数量达到最大线程数量,并且任务队列也已经放满了任务时,线程池的饱和策略就会生效,ThreadPoolExecutor定义了一些策略:
ArrayBlockingQueue:是基于数组实现的阻塞队列,在初始化已经开辟好空间,容量是固定的,会存在内存空间浪费和内存碎片的问题
LinkedBlockingQueue(无界队列):是基于链表实现的阻塞队列,当创建LinkedBlockingQueue时,可以指定队列容量,也可以不指定队列容量,如果指定了队列容量则为有界队列,如果未指定队列容量则默认为无界队列。如果为无界队列由于队列永远不会被放满(容量为Integer.MAX_VALUE),所以此时最多只能创建核心线程数的线程。
SynchronousQueue(同步队列):是一种没有容量的队列,每个插入操作一定要等待一个相应的删除操作(即任务放进队列后,被取出后才能继续放入),目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。
DelayedWorkQueue(延迟阻塞队列):其内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用“堆”的数据结构,可以保证每次出对的任务都是当前队列中执行时间最靠前的。
我们来介绍下,线程池处理任务的流程(即线程池执行execute的流程):
利用ThreadFactorBuilder来给线程池命名。即new ThreadFactorBuilder().setNameFormat().build();
ThreadFactory 接口是 Java 提供的一个创建线程的工厂接口,我们可以通过实现这个接口来定制线程的行为。ThreadFactory 接口里唯一的方法是 newThread(Runnable r),它会创建并返回一个新的线程对象。
在实现该接口时,可以通过重载 newThread() 方法来定制线程的名称。具体实现方式示例代码如下:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("my-thread-pool-%d").build();
ExecutorService executorService = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), namedThreadFactory);
我们在上面的学习中了解到使用线程池技术可以提高任务的响应速度,那是不是就说明线程池中线程的数量越大越好呢?其实理性的人都知道并不是这样的,虽然我们可以将线程池的数量设定的很大,但处理任务的CPU资源是不变的,这样就会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
问题1:什么是上下文切换
1、上下文切换是指:在多线程环境下,CPU从一个线程切换到另一个线程时,保存当前线程的上下文信息,并加载另一个线程上下文信息的过程。
而上下文信息指:线程在执行过程中自己的运行条件和状态,比如,程序计数器、栈信息、寄存器的值等(寄存器是指CPU内部一组高速存储单元,用于临时存储和操作数据)
2、多线程编程中一般线程的个数都大于 CPU 核的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。