Java多线程与线程池技术详解(九)

面对苦难的态度:《病隙碎笔》"不断的苦难才是不断地需要信心的原因,这是信心的原则,不可稍有更动。"

孤独与心灵的成长:《我与地坛》"孤独的心必是充盈的心,充盈得要流溢出来要冲涌出去,便渴望有人呼应他、收留他、理解他。"


目录

上一篇博客习题讲解

使用ReentrantLock实现生产者-消费者模式

为什么在某些情况下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多线程与线程池技术详解(八)

Java多线程与线程池技术详解(八)-CSDN博客文章浏览阅读428次,点赞19次,收藏8次。如果只有傻瓜才相信梦想,那么就叫我大傻瓜吧!"想走的路不好走,想做人不好做,都说是身不由己,不是废话么。己不由心,身又岂能由己!https://blog.csdn.net/speaking_me/article/details/144394346?spm=1001.2014.3001.5501

使用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的具体实现细节。
相关推荐
驭渊的小故事1 分钟前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客25 分钟前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18837 分钟前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn1 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su1 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
传奇开心果编程2 小时前
【Rust入门练中学】第5课:引用与借用
开发语言·rust·引用·编程语言理论·通用编程语言·借用
qetfw2 小时前
Bash 变量:export、位置参数与默认值展开验证
linux·开发语言·chrome·bash
电子云与长程纠缠2 小时前
UE5魂系游戏开发教程05
开发语言·学习·游戏·ui·ue5
cspttty3 小时前
2027风险管理岗准备路线:数学与应用数学如何补齐金融、SQL与Python
开发语言·python·金融
我命由我123453 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法