池化技术
程序运行的本质是占用系统的资源。在多线程的环境下,每次创建和释放线程都十分的消耗资源。
如果我们使用一个容器来管理线程,让线程空闲时不释放,而是放到容器里,需要使用到线程时不用去创建,而是从容器中拿去,这样就大大减少了资源的消耗。这就是池化技术的概念。
池化技术应用非常广泛,常见的还有:jdbc 连接池,内存池,对象池,常量池……
池化技术的好处:
- 降低资源的消耗
- 提高响应的速度(没有创建和销毁过程)
- 方便管理。(线程复用,控制最大并发数,管理线程使用)
池化技术的容器
池化技术的容器一般都选择队列。因为队列有先进先出(FIFO)的特性。
而线程池具有并发的限制,故要选择阻塞队列 BlockingQueue。
BlockingQueue 下有 ArrayBlockingQueue,LinkedBlockQueue,LinkedBlockingDeque(双向),ConcurrentLinkedQueue 等。
1. ArrayBlockingQueue
ArrayBlockingQueue 基于数组实现(循环队列)
。其通过 ReetrantLock 和 Condition 保证并发的安全性。
其有两个成员,putIndex(队尾),takeIndex(对头)。
putIndex 就是执行数组中上一个添加完元素的位置的下一个地方,如果 putIndex == length-1,则会以 0 开始。这就是循环队列的思想。
2. LinkedBlockingQueue
LinkedBlockingQueue 是基于链表实现,并且使用分离锁的技术来实现并发下的安全。
使用两个锁 takeLock 和 putLock,并且各自分离,互不影响。
当添加元素时,takeLock 会先锁住,然后再添加元素,并且去判断当前队列是否已满,如果满了则会 wait() 当前线程,即一直不释放锁,而其他添加元素的线程就只能一直等待。
当拿出元素时,takeLock 会先锁住,然后将元素拿出,并且判断当前是否队空,如果队空的话会 wait() 阻塞当前线程,使其他拿出元素的线程等待。添加元素的线程会去唤醒这个拿出元素的等待线程。同理,这个拿出元素的线程也会去唤醒添加元素的等待线程。
为什么能够让这两个锁各自分离且互不影响?
因为可以看作两个线程维护的是两个链表,并且当两个线程拿到同一个节点时,此时为队空,即会停止掉一个线程的并发,其他情况下,两个线程拿到的都不是同一个节点。
SynchronousQueue
SynchronousQueue 也是一个队列,但它的特别之处在于它内部没有容器。一个生产线程,当它生产成品后,如果没有人要消费,那么生产的线程也必须阻塞,不能继续生产。(只有一个容量的队列)。
队列的方法
功能 | 抛出异常 | 不跑出异常(返回值) | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer |
移除 | remove | poll | take | poll |
判断队首 | element | peek | - | - |
- 抛出异常:当队空或队满进行拿出或添加时会抛出异常。
- 返回值:当队空或队满进行拿出或添加时不是通过抛出异常来反馈,而是通过返回 true 或 false 来反馈。
- 阻塞等待:若队空或队满时不能拿出或添加时线程会继续等待,直到能操作位置。
- 超时等待:和阻塞等待类似,但有时间限制,如果超出时间,则会自动退出等待。
线程池分类
1. newFixedThreadPool(corePoolSize)
初始化一个指定线程数的线程池,使用 LinkedBlockingQueue 作为阻塞队列。
特点:即使线程池中没有可执行任务时,也不会释放线程。
2. newCachedThreadPool()
初始化一个可以缓存线程的线程池,默认缓存时间是 60s,线程池的线程数可达到 Integer.MAX_VALUE,使用 SynchronousQueue
作为阻塞队列。
特点:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。
因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致性能下降。
3. newSingleThreadExecutor()
初始化只有一个线程的线程池,使用 LinkedBlockingQueue 作为阻塞队列。
特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行。
4. newScheduledThreadPool()
初始化的线程池可以在指定的时间周期内执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
线程池的底层实现类
上述的线程池都是 jdk 帮我们封装好的。我们可以通过调用 Executors 类来创建上述线程池。
但阿里公约:线程池不允许使用 Excutors 去创建,而是通过底层类 ThreadPoolExecutor 的方式。这样的处理方式可以让开发人员更加明确线程池的运行规则,规避资源耗尽的风险。
上述的线程池也都是用 ThreadPoolExecuotr 来创建。
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
---
public static ExecutorService newFixedThreadPool(int nThreads){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
---
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()));
}
ThreadPoolExecutor 7 大参数
- corePoolSize:线程池初始的容量。
- maximumPoolSize:线程池最大容量。
- keepAliveTime:线程空闲时存活的时间。
- unit:keepAliveTime 的时间单位。
- workQueue:用来存放线程的阻塞队列。
- handler:拒绝策略(当队列满时继续添加时的策略)
- AbortPolicy:直接抛出异常,默认策略。
- CallerRunsPolicy:用调用者所在的线程执行任务。
- DiscardOldestPolicy:丢弃阻塞队列中靠前的任务,执行当前任务。
- DiscardPolicy:直接丢弃任务。