线程池
普通线程池
ThreadPoolExecutor
使用场景
1. 处理大量短暂异步任务
当应用程序需要处理大量短暂的异步任务时,使用线程池可以显著提高性能。线程池避免了为每个任务创建新线程的开销,因为它可以重用已经存在的线程。
2. 服务器处理客户端请求
在基于多线程的服务器应用程序中,如Web服务器或应用服务器,线程池被用于管理来自客户端的大量并发请求。线程池允许服务器在有限的资源下更有效地处理请求。
3. 并行数据处理
在需要并行处理大量数据的应用中(例如,在数据分析、科学计算中),线程池可以有效地将任务分配给多个线程,加快处理速度。
4. 定时和周期性任务执行
使用定时线程池(如ScheduledExecutorService
)可以方便地执行定时或周期性任务,这对于需要定期执行任务的应用程序(如定时清理资源、定时备份数据)非常有用。
5. 资源密集型任务
对于资源密集型的任务(如图像处理、大数据计算),线程池可以帮助限制并发执行的任务数量,防止因资源过度使用而导致的系统崩溃。
6. 异步IO操作
在进行异步IO操作时(如文件读写、网络通信),线程池可以用于管理和优化处理这些操作的线程。
7. 实现后台服务功能
对于需要持续运行的后台服务(如日志记录、系统监控),线程池提供了一个有效的机制来处理这些持续的或定期的任务。
注意事项
- 适当选择线程池类型和大小:根据应用程序的需求选择合适的线程池类型和大小,以避免资源浪费或性能瓶颈。
- 合理管理线程池的生命周期:确保线程池在不再需要时被正确关闭,以释放系统资源。
- 避免线程池过度使用:过多的线程可能会导致系统上下文切换频繁,反而降低性能。
使用线程池不仅可以提高性能,还可以提高程序的可维护性和可伸缩性。
工作流程
参数分析
1. 核心线程数 (corePoolSize
)
- 定义:线程池中始终保持活跃的线程数量,即使它们处于空闲状态。
- 作用:保证线程池中有足够的线程处理任务,提高响应速度。
- 选择依据:取决于你希望线程池对任务的响应速度以及系统资源的限制。
2. 最大线程数 (maximumPoolSize
)
- 定义:线程池中允许存在的最大线程数量。
- 作用:控制线程池的规模,防止因过多线程而消耗过多资源。
- 选择依据:基于任务的性质(CPU密集型、IO密集型等)和系统资源来确定。
3. 保持活跃时间 (keepAliveTime
)
- 定义:当线程数量超过核心线程数时,这些多余的线程在空闲时将等待新任务的最长时间。
- 作用:合理利用资源,当线程池空闲时减少资源消耗。
- 选择依据:根据任务的执行频率和资源管理策略来调整。
4. 时间单位 (TimeUnit
)
- 定义:
keepAliveTime
参数的时间单位,如TimeUnit.SECONDS
。 - 作用:指定
keepAliveTime
的精度。 - 选择依据:根据对时间精度的需求确定。
5. 工作队列 (workQueue
)
-
定义:用于存放等待执行的任务的阻塞队列。
-
作用:缓存任务,当所有核心线程都忙时,新任务会在队列中等待执行。
-
选择依据
:
- 直接交付(
SynchronousQueue
):适用于任务量少但需要立即执行的场景。 - 无界队列(
LinkedBlockingQueue
):适合任务执行时间短或任务量非常大的场景。 - 有界队列(
ArrayBlockingQueue
):适合任务量大但资源有限的场景。
- 直接交付(
6. 线程工厂 (ThreadFactory
)
- 定义:用于创建新线程的工厂。
- 作用:自定义线程创建方式,如设置线程名、优先级等。
- 选择依据:是否需要自定义线程的创建方式。
7. 拒绝策略 (RejectedExecutionHandler
)
-
定义:当任务无法提交给线程池执行时(例如,线程池已关闭或已达到最大容量)的处理策略。
-
作用:定义当线程池无法处理新任务时的行为。
-
选择依据
:
- 抛出异常(
AbortPolicy
):默认策略,直接抛出异常。 - 在调用者线程中运行任务(
CallerRunsPolicy
):降低新任务提交速度,减轻线程池压力。 - 丢弃任务(
DiscardPolicy
):直接丢弃新提交的任务。 - 丢弃队列中最旧的任务(
DiscardOldestPolicy
):丢弃等待队列中最旧的任务,并尝试提交新任务。
- 抛出异常(
示例代码
java复制代码import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = 4;
int maximumPoolSize = 10;
long keepAliveTime = 5000;
TimeUnit unit = TimeUnit.MILLISECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
executor.execute(() -> {
System.out.println("执行任务");
});
// 关闭线程池
executor.shutdown();
}
}
自定义线程池时,需要综合考虑上述参数,以确保线程池既能满足性能需求,又不会消耗过多的系统资源。
executorService
newCachedThreadPool
newCachedThreadPool
是 Java java.util.concurrent
包中 Executors
类的一个方法,用于创建一个线程池。这个线程池的特点是它在需要时创建新线程,并且在线程空闲一段时间后(默认为 60 秒)将其回收。这种类型的线程池特别适用于那些生命周期较短的异步任务。
特点
- 动态线程分配:根据任务的数量动态地创建和销毁线程。
- 空闲线程回收:如果线程超过一定时间(默认60秒)没有任务执行,则会被回收。
- 无限线程池:理论上,这种线程池可以为每个任务创建一个线程,不过实际上受到系统资源的限制。
使用场景
- 适用于许多短期异步任务的场景。
- 当任务到来的频率不固定,或者任务数量大幅波动时,使用
newCachedThreadPool
较为合适。
示例代码
java复制代码import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int threadNum = i;
executor.execute(() -> {
System.out.println("Task " + threadNum + " is running");
// 任务代码
});
}
executor.shutdown(); // 关闭线程池
}
}
注意事项
- 这种类型的线程池可能会创建大量线程,从而消耗大量系统资源,尤其是在任务数非常多的情况下。
- 如果使用不当,可能会导致系统性能降低甚至崩溃。
总的来说,newCachedThreadPool
提供了一种灵活的线程池管理方式,特别适合处理多个短暂异步任务。但在使用时也需要注意其对资源的潜在影响。
ThreadFactory
使用 ThreadFactory
可以自定义线程的各种属性,比如线程名称、优先级、是否为守护线程等。这在需要诊断线程行为或调整线程特性时非常有用。
例如,以下是使用自定义 ThreadFactory
的 newCachedThreadPool
的简单示例:
java复制代码import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ExecutorService;
public class ThreadPoolExample {
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactory() {
private int count = 1;
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + count++);
return thread;
}
};
ExecutorService executor = Executors.newCachedThreadPool(threadFactory);
executor.execute(() -> {
System.out.println("线程名称: " + Thread.currentThread().getName());
});
executor.shutdown();
}
}
newFixedThreadPool
在Java中,Executors.newFixedThreadPool(int nThreads)
是一个工具方法,用于创建一个固定线程数的线程池。在这种线程池中,即使有空闲的线程,线程池的大小也不会变化,这有助于控制并发水平。当所有线程都在忙时,额外的任务会在队列中等待。
下面是一个简单的使用 newFixedThreadPool
的教学示例:
- 导入必要的类: 首先,需要导入
java.util.concurrent.Executors
和java.util.concurrent.ExecutorService
。 - 创建固定大小的线程池: 使用
Executors.newFixedThreadPool(int nThreads)
创建一个具有固定线程数量的线程池。 - 提交任务: 使用
ExecutorService.execute(Runnable)
或ExecutorService.submit(Callable)
提交任务给线程池。 - 关闭线程池: 完成任务后,应该使用
ExecutorService.shutdown()
关闭线程池。
下面是一个具体的代码示例:
java复制代码import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务给线程池
for (int i = 0; i < 5; i++) {
int taskId = i;
executorService.execute(() -> {
System.out.println("执行任务 " + taskId + ",线程名称: " + Thread.currentThread().getName());
});
}
// 关闭线程池
executorService.shutdown();
}
}
在这个示例中,创建了一个固定大小为3的线程池。即使提交了5个任务,也只有3个任务会同时执行,其余的任务会在队列中等待,直到有线程变得可用。通过 Thread.currentThread().getName()
可以看到正在执行每个任务的线程的名称。
使用固定线程池是并发编程中的一个常见做法,尤其适用于处理固定数量并发任务的场景。
newSingleThreadPool
Executors.newSingleThreadExecutor()
在Java中用于创建一个单线程的线程池。这种类型的线程池有一些特定的用途和特点,下面将分别介绍其概念、使用场景和一个简单的使用教程。
介绍
newSingleThreadExecutor
创建的是一个只有一个工作线程的线程池。这意味着所有提交到该线程池的任务将在一个单独的线程中顺序执行,这有助于确保任务之间的顺序性和一致性。
特点
- 顺序执行:所有任务按照提交的顺序执行。
- 单线程:只有一个线程处理任务,避免了多线程的并发问题。
- 任务队列:如果有多个任务被提交,那些不能立即执行的任务会在队列中等待。
- 异常处理:如果执行的任务抛出异常,线程池会创建一个新的线程来替换它,确保后续任务继续执行。
使用场景
newSingleThreadExecutor
最适合的场景是需要顺序执行任务,且每个任务之间不应该由于线程上下文切换造成干扰的情况。例如:
- 顺序执行任务:当需要任务按提交顺序执行时。
- 资源同步:需要同步访问某些资源,但又不想处理复杂的同步机制时。
- 简化线程管理:当需要使用多线程,但又希望避免多线程并发问题的复杂性时。
教程
以下是如何使用 newSingleThreadExecutor
的基本步骤:
-
创建单线程池:
java复制代码ExecutorService executor = Executors.newSingleThreadExecutor();
-
提交任务: 使用
execute(Runnable)
提交任务,或者使用submit(Callable<T> 或 Runnable)
提交并获得返回值。java复制代码executor.execute(new Runnable() { public void run() { // 任务内容 } }); // 或 Future<?> future = executor.submit(new Callable<Object>() { public Object call() { // 任务内容 return null; } });
-
处理返回值(如果使用了
submit
):java复制代码try { Object result = future.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
-
关闭线程池:
java复制代码executor.shutdown();
注意事项
- 在任务执行完毕后,应该关闭线程池以释放资源。
newSingleThreadExecutor
对于处理异常情况具有一定的优势,因为它会在后台替换线程,但异常处理仍然需要程序员的注意。- 对于需要大量并行处理的场景,单线程池可能不是最佳选择,因为它的处理能力有限。
ScheduledExecutorService
ScheduledExecutorService
主要方法
- 创建线程池:
Executors.newScheduledThreadPool(int corePoolSize)
: 创建一个ScheduledExecutorService
,其核心线程数为corePoolSize
。
- 安排任务:
schedule(Runnable command, long delay, TimeUnit unit)
: 安排在指定延迟后执行的单次任务。schedule(Callable<V> callable, long delay, TimeUnit unit)
: 安排在指定延迟后执行的单次任务,并返回结果。
- 安排重复执行任务:
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
: 安排以固定频率重复执行的任务。任务按照指定的周期period
执行,不考虑任务的实际消耗时间。scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
: 安排在每次执行结束和下次执行开始之间有固定延迟的任务。delay
是一次执行终止和下一次执行开始之间的延迟。
- 关闭线程池:
shutdown()
: 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。shutdownNow()
: 尝试停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
newScheduledThreadPool
1. 安排单次任务
使用 schedule
方法可以在指定延迟后执行一次性任务。例如:
java复制代码ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
System.out.println("单次任务 - " + System.currentTimeMillis());
}, 2, TimeUnit.SECONDS);
在这个例子中,任务将在2秒后执行。
2. 安排固定频率的重复任务
scheduleAtFixedRate
方法用于安排任务以固定频率执行。例如:
java复制代码scheduler.scheduleAtFixedRate(() -> {
System.out.println("固定频率重复任务 - " + System.currentTimeMillis());
}, 1, 3, TimeUnit.SECONDS);
这里,任务首次执行延迟1秒,之后每3秒执行一次。
3. 安排固定延迟的重复任务
scheduleWithFixedDelay
方法安排在每次执行结束和下次执行开始之间有固定延迟的任务。例如:
java复制代码scheduler.scheduleWithFixedDelay(() -> {
System.out.println("固定延迟重复任务 - " + System.currentTimeMillis());
try {
Thread.sleep(2000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 1, 3, TimeUnit.SECONDS);
在这个例子中,任务首次执行延迟1秒,之后每次执行完成后等待3秒再次执行。
4. 关闭线程池
使用 shutdown
或 shutdownNow
方法关闭线程池。例如:
java复制代码scheduler.shutdown();
这会在已提交的任务执行完后关闭线程池,而不接受新的任务。
总结
ScheduledExecutorService
提供了灵活的方法来安排和管理延迟执行或周期性执行的任务。根据具体需求选择合适的方法,可以有效地管理任务的执行计划。在使用过程中,还需要注意适时地关闭线程池,以释放资源。
newSingleThreadScheduledExecutor
这个方法创建的是一个单线程的 ScheduledExecutorService
。它具有以下特点:
- 单线程执行:所有任务在同一个线程中顺序执行。
- 定时和周期性任务:支持执行一次性任务,也支持执行定时或周期性的任务。
- 顺序保证:由于所有任务在单个线程中执行,因此可以保证任务之间的顺序性和一致性。
使用场景
- 当需要顺序执行多个定时或周期性任务时。
- 当任务间的相互干扰需要最小化时。
- 当需要处理异常并保证后续任务不受影响时。
使用教程
-
创建单线程定时任务线程池:
java复制代码ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
-
安排任务执行:
-
安排单次任务:
java复制代码singleThreadScheduledExecutor.schedule(new Runnable() { @Override public void run() { System.out.println("Task executed after a delay"); } }, 10, TimeUnit.SECONDS);
-
安排周期性任务:
java复制代码singleThreadScheduledExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("Periodic task executed every 5 seconds"); } }, 0, 5, TimeUnit.SECONDS);
-
-
关闭线程池:
-
任务执行完毕后,关闭线程池:
java复制代码singleThreadScheduledExecutor.shutdown();
-
注意事项
- 和其他定时任务服务一样,异常处理需要妥善管理,否则一次未捕获的异常可能会终止后续所有任务的执行。
- 在不再需要线程池时,应该调用
shutdown
方法关闭它,以释放资源。 - 考虑到只有一个线程负责所有任务,任务执行的时间过长可能会影响后续任务的准时执行。
秒杀实战
编程题目:模拟手机秒杀活动
背景
您已经有一个基础的秒杀系统实现,它模拟了多个客户(用户)通过一个线程池参与秒杀活动。现在,我们有一个具体的场景:共有20个客户参与秒杀,而秒杀的商品(手机)只有10个。
任务
您的任务是根据这个场景对现有的秒杀系统进行适当的修改和扩展,确保它可以正确且公平地处理这次秒杀活动。具体要求如下:
- 限制商品数量:确保只有10个手机可供秒杀,且一旦所有手机都被秒杀,立即通知其它仍在尝试的客户秒杀结束。
- 确保公平性:所有客户应有平等的机会参与秒杀,避免因线程调度不均导致的不公平现象。
- 优化性能:考虑使用合适的并发控制机制,提高程序在高并发环境下的性能。
- 异常处理:妥善处理可能出现的异常,保证程序的稳定性。
- 代码优化:重构代码,提高其可读性和可维护性。
package 商品秒杀;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 任务测试类
*/
public class MyTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 10, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(15));
for (int i = 0; i < 20; i++) {
MyTask mytask = new MyTask("客户" + i);
threadPoolExecutor.submit(mytask);
}
threadPoolExecutor.shutdown();
}
}
package 商品秒杀;
/**
* 任务类:
*
*/
public class MyTask implements Runnable {
private static int id = 10;
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
String name1 = Thread.currentThread().getName();
System.out.println(name + "正在使用" + name1 + "参与秒杀");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (MyTask.class) {
if (id > 0) {
System.out.println(name + "使用" + name1 + "秒杀" + id-- + "商品成功");
}else{
System.out.println(name + "使用" + name1 + "秒杀失败");
}
}
}
}
取钱实战
package 取款;
public class MyTask implements Runnable {
// 用户名
private String username;
// 总额
private static double amout = 1000;
// 取款
private double money;
public MyTask(String username, double money) {
this.username = username;
this.money = money;
}
@Override
public void run() {
synchronized (MyTask.class) {
if (money <= amout) {
amout = amout - money;
System.out.println(username + "使用" + Thread.currentThread().getName() + "取款" + money + "成功" + "|余额:" + amout);
} else {
System.out.println(username + "使用" + Thread.currentThread().getName() + "取款" + money + "失败" + "|余额:" + amout);
}
}
}
}
package 取款;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class MyTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2, new ThreadFactory() {
private int id = 0;
@Override
public Thread newThread(Runnable r) {
id++;
Thread thread = new Thread(r, "ATM" + id);
return thread;
}
});
executorService.submit(new MyTask("张三", 800));
executorService.submit(new MyTask("李四", 300));
executorService.shutdown();
}
}