面对苦难的态度:《病隙碎笔》"不断的苦难才是不断地需要信心的原因,这是信心的原则,不可稍有更动。"
孤独与心灵的成长:《我与地坛》"孤独的心必是充盈的心,充盈得要流溢出来要冲涌出去,便渴望有人呼应他、收留他、理解他。"
目录
为什么在某些情况下ReentrantLock的表现优于synchronized?
[Shutdown() vs ShutdownNow()](#Shutdown() vs ShutdownNow())
[Future 和 FutureTask](#Future 和 FutureTask)
[第9章 Tomcat线程池技术](#第9章 Tomcat线程池技术)
[9.1 自定义 ThreadPoolExecutor](#9.1 自定义 ThreadPoolExecutor)
[9.2 Tomcat任务队列](#9.2 Tomcat任务队列)
[9.3 Tomcat任务线程](#9.3 Tomcat任务线程)
[9.4 Tomcat任务线程工厂](#9.4 Tomcat任务线程工厂)
[9.5 Tomcat连接器与线程池](#9.5 Tomcat连接器与线程池)
[9.6 创建 Tomcat 线程池](#9.6 创建 Tomcat 线程池)
[9.7 Web服务器异步环境](#9.7 Web服务器异步环境)
[9.8 Web服务器 NIO](#9.8 Web服务器 NIO)
[9.9 本章习题](#9.9 本章习题)
上一篇博客习题讲解
Java多线程与线程池技术详解(八)
使用ReentrantLock实现生产者-消费者模式
生产者-消费者模式是并发编程中的经典问题,它涉及到两个或多个线程之间的协调工作。为了确保数据的一致性和线程的安全性,通常会使用锁机制来控制对共享资源的访问。下面是一段使用ReentrantLock
实现生产者-消费者模式的示例代码:
import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ProducerConsumerExample { private final int MAX_SIZE = 5; private final LinkedList<Integer> list = new LinkedList<>(); private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void produce(int value) throws InterruptedException { lock.lock(); try { while (list.size() == MAX_SIZE) { System.out.println("Buffer is full, waiting..."); notFull.await(); } list.add(value); System.out.println("Produced: " + value); notEmpty.signalAll(); } finally { lock.unlock(); } } public Integer consume() throws InterruptedException { lock.lock(); try { while (list.isEmpty()) { System.out.println("Buffer is empty, waiting..."); notEmpty.await(); } Integer value = list.removeFirst(); System.out.println("Consumed: " + value); notFull.signalAll(); return value; } finally { lock.unlock(); } } }
这段代码中,我们创建了一个固定大小的缓冲区,并通过ReentrantLock
和两个Condition
对象(notFull
和notEmpty
)来管理生产和消费的过程。
为什么在某些情况下ReentrantLock的表现优于synchronized?
ReentrantLock
提供了比synchronized
更灵活的功能,例如可以尝试获取锁、支持公平锁、允许锁中断等特性。此外,在高并发场景下,ReentrantLock
的性能可能优于synchronized
,因为它避免了线程进入内核态的阻塞状态。不过需要注意的是,在低并发的情况下,synchronized
的性能表现可能会更好。
设计一个场景,说明何时应该选择使用读写锁而不是普通的互斥锁
假设有一个缓存系统,其中读取操作远远多于写入操作。在这种情况下,如果使用普通的互斥锁,则每次读取时都会阻止其他读取操作的发生,即使它们不会相互影响。而使用读写锁(如ReentrantReadWriteLock
),则可以在没有写入操作发生时允许多个读取操作同时进行,从而提高了系统的并发度和响应速度。
实现一个简单的银行账户类
对于银行账户类,我们可以分别使用synchronized
和ReentrantLock
来保证线程安全。以下是两种实现方式:
使用synchronized
关键字:
public class BankAccountSynchronized { private double balance; public synchronized void deposit(double amount) { // 存款逻辑 } public synchronized boolean withdraw(double amount) { // 取款逻辑 return true; } }
使用ReentrantLock
:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BankAccountReentrantLock { private double balance; private final Lock lock = new ReentrantLock(); public void deposit(double amount) { lock.lock(); try { // 存款逻辑 } finally { lock.unlock(); } } public boolean withdraw(double amount) { lock.lock(); try { // 取款逻辑 return true; } finally { lock.unlock(); } } }
两者的主要区别在于synchronized
是隐式锁,自动管理锁的获取与释放;而ReentrantLock
需要显式地调用lock()
和unlock()
方法来控制锁的行为。
公平锁与非公平锁
- 公平锁:所有等待线程按照请求锁的顺序获得锁,这有助于防止饥饿现象的发生,但吞吐量较低。
- 非公平锁:允许新到达的线程插队,即有可能跳过已经在等待的线程直接获得锁,这种方式能提高吞吐量,但在极端情况下可能导致部分线程长时间得不到执行机会。
例如,在一个高频交易系统中,为了最大化吞吐量,可以选择使用非公平锁;而在一个任务调度系统中,为了保证每个任务都能得到及时处理,可能更适合采用公平锁。
Shutdown() vs ShutdownNow()
shutdown()
方法会停止接收新的任务并将试图终止所有正在运行的任务,但它不会立即终止已提交的任务。相反,shutdownNow()
将尝试取消所有未开始的任务,并且会中断正在执行的任务。因此,shutdownNow()
更激进,可能会导致一些任务被中途打断,适用于紧急情况下的快速关闭。
Future 和 FutureTask
Future
接口表示异步计算的结果,提供了检查计算是否完成、等待计算完成以及获取结果的方法。FutureTask
是一个实现了Runnable
和Future
接口的具体类,它可以包装一个Callable或Runnable对象,使得可以通过调用其run()
方法启动任务,并通过get()
方法获取结果或等待任务完成。此外,还可以调用cancel(boolean mayInterruptIfRunning)
来尝试取消任务。
创建可暂停和恢复所有线程池任务的系统
要实现这样一个系统,可以考虑为每个任务添加一个状态标志位,用于指示任务是否应该暂停。当接收到暂停指令时,所有任务都将检查自己的状态并根据需要暂停执行。恢复时,再次检查状态以决定是否继续执行。需要注意的是,这种设计可能会引入额外的复杂性,比如如何同步状态变更以及处理潜在的死锁问题。
知识讲解
第9章 Tomcat线程池技术
9.1 自定义 ThreadPoolExecutor
Tomcat的线程池是基于Java的ThreadPoolExecutor
实现的,但为了适应Web服务器的需求,它做了许多定制化处理。在创建自定义的ThreadPoolExecutor
时,可以指定核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲线程存活时间(keepAliveTime)、任务队列(workQueue)等参数。Tomcat中的ThreadPoolExecutor
与标准JDK版本不同,它增加了对提交任务计数的支持,并且在执行任务失败时会尝试将任务重新加入到任务队列中。
// 自定义ThreadPoolExecutor构造函数 public class CustomThreadPoolExecutor extends ThreadPoolExecutor { public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); // 预热所有核心线程 prestartAllCoreThreads(); } }
9.2 Tomcat任务队列
Tomcat的任务队列并非直接使用JDK提供的阻塞队列,而是使用了一个名为TaskQueue
的类,它是LinkedBlockingQueue
的一个子类。这个队列实现了特殊的逻辑:当线程池中的线程数量小于最大线程数时,它会优先创建新的线程来处理任务而不是将任务放入队列;只有在线程数达到最大值后才会考虑将任务放入队列。
// TaskQueue 类的部分实现 public class TaskQueue extends LinkedBlockingQueue<Runnable> { @Override public boolean offer(Runnable o) { // 如果线程池大小未达到最大,则返回false,表示队列已满 if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) { return false; } // 否则调用父类方法添加任务 return super.offer(o); } }
9.3 Tomcat任务线程
每个任务线程都是由TaskThreadFactory
创建出来的,它们继承自Thread
类,并且可以根据需要设置线程名称前缀、守护状态以及优先级。这些线程负责从任务队列中取出任务并执行。
// TaskThreadFactory 创建线程的方法 public class TaskThreadFactory implements ThreadFactory { private final String namePrefix; private final boolean daemon; private final int threadPriority; public TaskThreadFactory(String namePrefix, boolean daemon, int threadPriority) { this.namePrefix = namePrefix; this.daemon = daemon; this.threadPriority = threadPriority; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement()); t.setDaemon(daemon); t.setPriority(threadPriority); return t; } }
9.4 Tomcat任务线程工厂
如上所示,TaskThreadFactory
用于创建线程实例,并允许开发者配置线程的名字、是否为守护进程及优先级。
9.5 Tomcat连接器与线程池
Tomcat的连接器(Connector)负责监听客户端请求,并通过线程池分配线程来处理这些请求。根据不同的I/O模型(BIO/NIO/APR),可以选择不同的连接器实现方式。例如,默认情况下NIO模式下使用的NioEndpoint
会创建一个或多个Acceptor线程来接收新连接,并将其交给Poller线程进行读写操作。
9.6 创建 Tomcat 线程池
在Tomcat启动过程中,AbstractEndpoint#createExecutor()
方法会被调用来初始化线程池。这里不仅设置了线程池的基本属性,还预热了所有的核心线程以确保一旦有请求到来就能立即得到处理。
public void createExecutor() { internalExecutor = true; TaskQueue taskqueue = new TaskQueue(); TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60L, TimeUnit.SECONDS, taskqueue, tf); taskqueue.setParent((ThreadPoolExecutor) executor); }
9.7 Web服务器异步环境
对于支持异步Servlet的应用程序来说,Tomcat提供了AsyncContext
机制,使得可以在非阻塞的方式下调用业务逻辑,从而提高系统的并发处理能力。
下面是一个简单的例子展示了如何使用AsyncContext
:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { final AsyncContext asyncContext = request.startAsync(); asyncContext.start(() -> { try { // 调用业务方法 businessMethod(asyncContext.getResponse()); asyncContext.complete(); } catch (Exception e) { asyncContext.setError(e); asyncContext.complete(); } }); } private void businessMethod(HttpServletResponse response) throws Exception { // 模拟长时间运行的任务 Thread.sleep(5000); response.getWriter().println("Hello World!"); }
9.8 Web服务器 NIO
Tomcat的NIO实现依赖于Java NIO库,它允许单个线程管理多个套接字连接。这减少了所需的线程数,并提高了性能。NioEndpoint
类包含了对NIO特性的具体实现,包括但不限于选择器(Selector)、通道(Channel)和缓冲区(Buffer)的操作。
// NioEndpoint 中的部分代码片段 @Override protected void startInternal() throws Exception { // 创建并启动Poller线程 poller = new Poller(); Thread pollerThread = new Thread(poller, getName() + "-Poller"); pollerThread.setPriority(threadPriority); pollerThread.setDaemon(true); pollerThread.start(); startAcceptorThreads(); }
9.9 本章习题
考虑到篇幅限制,此处不提供完整的练习题目,但是建议读者尝试完成以下任务来加深理解:
- 实现自己的
ThreadPoolExecutor
,并测试其行为。 - 修改
TaskQueue
的行为,使其在某些条件下拒绝接受新任务。 - 使用
AsyncContext
创建一个异步Servlet应用。 - 探索Tomcat源码中关于NIO的具体实现细节。