我们都知道在面试的过程中,关于线程池的问题,一直都是面试官比较注重的考点,现在也不会有面试官会选择去问创建线程都有哪些方式了,而更多的实惠关注到如何去使用线程池,今天了不起就来和大家说说线程池。
Java创建线程池方式
在Java中,创建线程池主要使用java.util.concurrent包下的Executors类。这个类提供了几种静态工厂方法,用于创建和管理不同类型的线程池。以下是一些常见的创建线程池的方式:
1.Fixed Thread Pool(固定线程池)
- 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。
- 创建方法:Executors.newFixedThreadPool(int nThreads)
2.Cached Thread Pool(缓存线程池)
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
- 创建方法:Executors.newCachedThreadPool()
3.Single Thread Executor(单线程执行器)
- 创建一个使用单个工作线程的 Executor,以无界队列方式来运行该线程。(注意,如果单个线程始终因为等待新任务而处于非活动状态,则在现行线程终止之前,它可能无法终止。)但是,如果线程因为失败而终止,那么会有一个新的线程来替代它。单个线程的优势在于,你无需处理对线程生命周期的管理。
- 创建方法:Executors.newSingleThreadExecutor()
4.Scheduled Thread Pool(计划线程池)
- 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- 创建方法:Executors.newScheduledThreadPool(int corePoolSize)
5.自定义线程池
除了使用Executors类提供的静态工厂方法创建线程池外,还可以通过实例化ThreadPoolExecutor类来自定义线程池。这种方式提供了更多的灵活性,允许你设置线程池的核心参数,如核心线程数、最大线程数、线程存活时间、任务队列等。
示例代码:
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
// 使用线程池执行任务...
}
}
非自定义线程池的缺点
我们先来看看 Executors 当中的几个方法,也就是上面了不起给大家写的除了自定义线程池的几个方法。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
在源码中有一个类,我们明显的看到了队列的身影,那就是 LinkedBlockingQueue。
它实现了一个基于链接节点的可选容量的阻塞队列。此队列按 FIFO(先进先出)排序元素。队列的头部是在队列中存在时间最长的元素,队列的尾部是在队列中存在时间最短的元素。新元素总是插入到队列的尾部,而检索操作(如 take 和 poll)总是从队列的头部开始。
LinkedBlockingQueue 是一个线程安全的队列,它内部使用了锁和条件变量来保证多线程环境下的正确性和一致性。因为它是阻塞队列,所以它可以用于生产者和消费者模型,在生产者线程和消费者线程之间传递数据。
LinkedBlockingQueue 的主要特点就几个
- 容量可选
- 阻塞操作
- 非阻塞操作
- 线程安全
- 高效的并发性能
为什么说容量可选呢?因为我们如果单独使用这个LinkedBlockingQueue 那么你可以在创建 LinkedBlockingQueue 时指定一个容量,这将限制队列中可以存储的元素数量。如果未指定容量,则队列的容量将是 Integer.MAX_VALUE。当队列满时,任何尝试插入元素的线程都将被阻塞,直到队列中有空间可用。
而阻塞操作则是他提供了阻塞的 put 和 take 方法。put 方法用于添加元素到队列中,如果队列已满,则调用线程将被阻塞直到队列有空闲空间。take 方法用于从队列中移除并返回头部元素,如果队列为空,则调用线程将被阻塞直到队列中有元素可用。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
......
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
.....
我们看一个使用LinkedBlockingQueue的示例:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println("Produced: " + i);
queue.put(i);
Thread.sleep(200); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer item = queue.take();
System.out.println("Consumed: " + item);
Thread.sleep(500); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
// 注意:这里的 consumer 线程是一个无限循环,所以它不会自然结束。
// 在实际应用中,你需要有一个明确的停止条件来结束消费者线程。
}
}
说到这里感觉说多了,我们回归正题,如果我们使用标准的 newCachedThreadPool 方法,如果线程数设置和任务数不能够配合起来,就比如说设置的线程数是一定的,这个时候,任务数量越多,就会慢慢的进入到队列LinkedBlockingQueue中,队列的话,任务越多,占用的内存越多,最终就非常容易耗尽内存,导致OOM。
所以我们不推荐直接使用 Executors 来创建线程池,但是我们更推荐使用 ThreadpoolExecutor创建线程池。原因就是如下的几点:
1.资源控制:ThreadPoolExecutor 允许你明确控制并发线程的最大数量,防止因为创建过多的线程而耗尽系统资源。通过合理地设置线程池的大小,可以平衡资源利用率和系统性能。
2.线程复用:线程池中的线程可以被多个任务复用,这减少了在创建和销毁线程上花费的时间以及开销,提高了系统的响应速度。
3.任务队列:ThreadPoolExecutor 内部维护了一个任务队列,当线程池中的线程都在工作时,新提交的任务会被放在队列中等待执行。这提供了一种缓冲机制,可以平滑处理突发的高并发任务。
4.灵活性:ThreadPoolExecutor 提供了多种配置选项,如核心线程数、最大线程数、线程存活时间、任务队列类型等,这些选项可以根据具体的应用场景进行调整,以达到最佳的性能和资源利用率。
5.异常处理:当线程池中的线程因为未捕获的异常而终止时,ThreadPoolExecutor 会创建一个新的线程来替代它,从而保持线程池的稳定性。此外,你也可以通过提供自定义的 ThreadFactory 来控制线程的创建过程,例如设置线程的名称、优先级、守护状态等。
6.可扩展性:ThreadPoolExecutor 的设计是基于策略的,它使用了多个接口和抽象类来定义线程池的行为,这使得它很容易通过扩展或替换某些组件来适应不同的需求。
7.与Java并发库集成:ThreadPoolExecutor 是 Java 并发库 java.util.concurrent 的一部分,这个库提供了丰富的并发工具和类,如锁、信号量、倒计时器、阻塞队列等,这些都可以与 ThreadPoolExecutor 无缝集成,简化多线程编程的复杂性。
8.性能监控和调优:ThreadPoolExecutor 提供了一些有用的方法,如 getTaskCount()、getCompletedTaskCount()、getPoolSize() 等,这些方法可以帮助你监控线程池的运行状态,从而进行性能调优。
所以你了解了么?