线程池的选择与配置
- 选择合适的线程池:
- FixedThreadPool:适用于已知并发请求数量且相对稳定的场景。例如,如果预估并发请求数在100左右,且系统资源有限,可以创建一个固定大小为100的FixedThreadPool。它的优点是线程数量固定,不会因为过多请求导致线程资源耗尽,缺点是如果请求处理时间过长,可能会导致队列堆积。
- CachedThreadPool:适合处理大量短时间的突发请求。它会根据需要创建新线程,如果线程闲置一段时间(默认60秒),会被回收。例如在秒杀活动开始瞬间,大量请求涌入,CachedThreadPool可以快速创建线程来处理,但如果持续有大量请求,可能会消耗过多系统资源。
- ScheduledThreadPool:如果项目中有定时任务,比如定时清理缓存,定时更新数据等,就可以使用ScheduledThreadPool。它可以按照指定的延迟时间或周期执行任务。
- ThreadPoolExecutor:这是最灵活的线程池实现类,可以根据项目需求自定义线程池的核心线程数、最大线程数、存活时间等参数。在大多数复杂的网络后端开发场景下,推荐使用ThreadPoolExecutor。例如,核心线程数设置为CPU核心数的2倍,最大线程数设置为CPU核心数的4倍,这样既可以充分利用CPU资源,又能在高并发时适当增加处理能力。
- 配置线程池参数:
- 核心线程数:通常根据CPU核心数和任务类型来确定。对于I/O密集型任务,核心线程数可以设置为CPU核心数的2 - 3倍,因为I/O操作会使线程等待,此时多一些线程可以提高CPU利用率;对于CPU密集型任务,核心线程数可以设置为CPU核心数,避免过多线程竞争CPU资源。
- 最大线程数:要考虑系统资源,如内存等。如果线程数过多,会导致系统性能下降。一般在核心线程数的基础上适当增加,比如增加2 - 3倍。
- 线程存活时间:对于CachedThreadPool和ThreadPoolExecutor(当线程数超过核心线程数时),设置合理的线程存活时间,比如1 - 2分钟,可以及时回收闲置线程,释放资源。
- 任务队列:选择合适的任务队列。ArrayBlockingQueue是有界队列,可设置固定大小,能防止任务无限堆积;LinkedBlockingQueue是无界队列,适用于请求处理速度较快,不会导致队列过度膨胀的场景;SynchronousQueue不存储任务,直接将任务交给线程处理,适用于处理速度极快的场景。
线程安全的考虑
- 共享资源同步:
- 使用synchronized关键字:如果项目中有共享资源,比如共享的缓存数据,在访问和修改这些资源的方法或代码块上使用synchronized关键字。例如:
private static final Object lock = new Object();
public void updateSharedResource() {
synchronized (lock) {
// 对共享资源进行操作
}
}
- 使用Lock接口:相比synchronized,Lock接口提供了更灵活的锁控制,如可中断的锁获取、公平锁等。例如ReentrantLock:
private static final Lock lock = new ReentrantLock();
public void updateSharedResource() {
lock.lock();
try {
// 对共享资源进行操作
} finally {
lock.unlock();
}
}
- 线程安全的类:
- 使用线程安全的集合类:在处理并发数据时,使用线程安全的集合类,如ConcurrentHashMap代替HashMap,CopyOnWriteArrayList代替ArrayList等。例如:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
- 原子类:对于简单的共享变量操作,使用原子类,如AtomicInteger、AtomicLong等,它们提供了原子性的操作,无需额外的同步机制。例如:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
- 避免死锁:
- 按顺序加锁:如果在多个线程中需要获取多个锁,按照相同的顺序获取锁。例如,线程A和线程B都需要获取锁lock1和lock2,那么都先获取lock1,再获取lock2。
- 定时锁:使用Lock接口的tryLock方法,设置获取锁的超时时间,如果在规定时间内无法获取锁,则放弃获取,避免无限等待导致死锁。例如:
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 处理逻辑
} catch (InterruptedException e) {
// 处理中断异常
} finally {
lock.unlock();
}
}
具体设计思路
- 请求接收与分发:
- 使用Servlet或Netty等框架接收网络请求。例如在Servlet中,通过HttpServlet的doGet、doPost等方法接收请求。
- 将接收到的请求封装成任务,提交到线程池。可以定义一个任务类,实现Runnable或Callable接口,在任务类中处理具体的业务逻辑。
- 业务逻辑处理:
- 在任务类的run或call方法中,调用业务逻辑处理方法。这些方法要保证线程安全,按照上述线程安全的考虑进行设计。
- 如果业务逻辑涉及到数据库操作、文件操作等I/O操作,要注意合理使用连接池等资源,避免资源耗尽。
- 响应返回:
- 任务处理完成后,将处理结果返回给客户端。在Servlet中,可以通过HttpServletResponse将结果写入响应流;在Netty中,可以通过ChannelFuture将结果发送回客户端。
- 监控与调优:
- 使用JMX(Java Management Extensions)等工具监控线程池的运行状态,如线程池中的线程数量、任务队列大小、已完成任务数等。
- 根据监控结果,动态调整线程池的参数,优化系统性能,确保系统在高并发下稳定运行。