线程池实战——实现Web服务器

引言

作者在前面写了很多并发编程知识深度探索系列文章,反馈得知友友们收获颇丰,同时我也了解到友友们也有了对知识如何应用感到很模糊的问题。所以作者就打算写一个实战系列文章,让友友们切身感受一下怎么应用知识。这次续接上集,可以搭配线程池实战——数据库连接池-CSDN博客这篇文章一起学习。话不多说,开始吧!

在Web服务高速发展的当下,服务器作为承载业务流量的核心枢纽,其对并发请求的处理能力直接决定了用户体验的上限。尤其在浏览器多线程并发请求(如图片、脚本、样式文件同时加载)的场景中,传统单线程处理模式因频繁创建/销毁线程导致的资源浪费,成为系统性能的瓶颈。线程池技术顺势成为破局关键,通过预先创建线程并维持其循环执行任务的机制,显著降低线程调度开销,提升请求处理效率。本文将结合Java实现的极简Web服务器案例,深入解析线程池如何借助同步控制、任务队列与线程复用策略,构建高吞吐的请求处理链路,为理解高并发场景下的服务器性能优化提供可落地的技术路径与实践参考。

前置知识

1.Woker类

线程池中的线程并不是直接用的Thread类,而是定义了一个内部工作线程Worker类,实现了AQS以及Runnable接口,然后持有一个Thread类的引用及一个firstTask(创建后第一个要执行的任务),每个Worker线程启动后会执行run()方法,该方法会调用执行外层runWorker(Worker w)方法

线程池(如 Java 的 ThreadPoolExecutor)通过自定义工作线程类(Worker)来管理任务执行,这一设计有其核心目的和实现逻辑。

在博主之前这篇文章也讲解过《别再懵圈!一文读懂线程池源码逻辑》-CSDN博客

下面从设计动机、Worker 类结构、执行流程三个方面详细解释:

一、为什么不直接用 Thread 类?

  1. 复用线程资源
    若直接创建 Thread 对象,每个任务都需新建线程,任务结束后线程销毁,会带来频繁创建 / 销毁的开销。而 Worker 类通过包装 Thread,使线程可以循环执行多个任务(线程复用),显著提升性能。

  2. 精确控制线程状态
    Worker 类实现了 AQS(AbstractQueuedSynchronizer),可利用 AQS 的锁机制管理线程的中断状态(如tryLock()),防止任务执行期间被意外中断。

  3. 任务预取与执行分离
    Worker 持有firstTask引用,允许线程启动后立即执行初始化任务,之后再从任务队列中获取后续任务,实现任务的预取与执行逻辑分离。

二、Worker 类的核心结构

Worker 类的简化结构如下:

private final class Worker 
    extends AbstractQueuedSynchronizer 
    implements Runnable {
    
    final Thread thread;      // 实际执行任务的线程
    Runnable firstTask;       // 初始化任务(可为null)
    volatile long completedTasks;  // 记录完成的任务数
    
    Worker(Runnable firstTask) {
        setState(-1);        // 初始化状态,禁止中断
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    public void run() {
        runWorker(this);     // 调用外部ThreadPoolExecutor的方法
    }
    
    // AQS相关方法(锁机制实现)
    protected boolean isHeldExclusively() { ... }
    protected boolean tryAcquire(int unused) { ... }
    protected boolean tryRelease(int unused) { ... }
    public void lock() { ... }
    public boolean tryLock() { ... }
    public void unlock() { ... }
}

三、执行流程详解

  1. Worker 初始化与启动

    • 当线程池需要创建新线程时,会实例化 Worker 并传入firstTask(若有)。
    • Worker构造函数中会通过线程工厂创建一个新 Thread,并将自身(实现了 Runnable)作为任务传入。
  2. 线程启动后的执行路径

    // ThreadPoolExecutor内部
    execute(Runnable command) {
        // ...
        addWorker(command, true);  // 创建Worker并启动线程
    }
    
    private boolean addWorker(Runnable firstTask, boolean core) {
        // ...
        w = new Worker(firstTask);  // 创建Worker
        final Thread t = w.thread;  // 获取Worker持有的线程
        t.start();                  // 启动线程(触发Worker的run())
    }
    
  3. Worker 的 run () 方法与 runWorker ()

    • 线程启动后调用Worker.run(),而run()内部直接调用ThreadPoolExecutor.runWorker(Worker w)
    • runWorker()是核心逻辑,负责循环执行任务:
      final void runWorker(Worker w) {
          Thread wt = Thread.currentThread();
          Runnable task = w.firstTask;
          w.firstTask = null;
          w.unlock();  // 释放锁,允许中断
          boolean completedAbruptly = true;
          
          try {
              // 先执行firstTask,再从队列中获取任务
              while (task != null || (task = getTask()) != null) {
                  w.lock();  // 加锁,防止执行期间被中断
                  // ... 执行前检查线程状态
                  try {
                      beforeExecute(wt, task);
                      Throwable thrown = null;
                      try {
                          task.run();  // 执行实际任务
                      } finally {
                          afterExecute(task, thrown);
                      }
                  } finally {
                      task = null;
                      w.completedTasks++;
                      w.unlock();  // 释放锁
                  }
              }
              completedAbruptly = false;
          } finally {
              processWorkerExit(w, completedAbruptly);
          }
      }
      

四、关键点总结

  1. AQS 的作用

    • 通过 AQS 实现锁机制,确保:
      • 任务执行期间(runWorker中)不会被中断(除非线程池被 shutdownNow)。
      • 线程空闲时(从队列获取任务时)可响应中断。
  2. 任务获取逻辑

    • getTask()方法从任务队列中获取任务,支持超时退出(如核心线程超时或线程数超过核心数)。
    • 若队列为空且不满足创建新线程条件,线程会被回收。
  3. 线程复用机制

    • while循环使 Worker 线程在完成一个任务后,继续从队列获取下一个任务,直到线程池关闭或满足退出条件。

五、设计优势

  • 解耦线程管理与任务执行:Worker 类作为中间层,隔离了线程生命周期管理和任务调度逻辑。
  • 高效资源利用:通过线程复用减少开销,提升吞吐量。
  • 安全控制:利用 AQS 实现细粒度的锁控制,保障线程安全。

2.线程池技术及其示例

服务端程序经常面对客户端传入的短小任务(执行时间短、工作内容较为单一),需要快速处理并返回结果。如果服务端每次接收到一个任务就创建一个线程并执行,这在原型阶段是不错的选择。但是当成千上万的任务传入服务器时,如果仍采用一个任务一个线程的方式,将会创建数以万计的线程,这并非好的选择。因为这会使操作系统频繁进行线程上下文切换,增加系统负载,并且线程的创建和消亡都需要耗费系统资源,无疑会造成系统资源的浪费。

线程池技术能够很好地解决这个问题。它预先创建若干数量的线程,并设置用户不能直接控制线程的创建,在此前提下重复使用固定或较为固定数目的线程来完成任务的执行。这样做的好处是,一方面消除了频繁创建和消亡线程的系统资源开销,另一方面面对过量任务的提交能够平缓地劣化。

下面先看一个简单的线程池接口定义:

public interface ThreadPool<Job extends Runnable> {
    // 执行一个Job,这个Job需要实现Runnable接口
    void execute(Job job);
    
    // 关闭线程池
    void shutdown();
    
    // 增加工作者线程
    void addWorkers(int num);
    
    // 减少工作者线程
    void removeWorker(int num);
    
    // 得到正在等待执行的任务数量
    int getJobSize();
}

客户端可以通过execute(Job)方法将 Job 提交进线程池执行,而客户端自身不用等待 Job 的执行完成。除了execute(Job)方法以外,线程池接口还提供了增加或减少工作者线程以及关闭线程池的方法。这里工作者线程代表着一个重复执行 Job 的线程,而每个由客户提交的 Job 都将进入一个工作队列中等待工作者线程的处理。

接下来是线程池接口的默认实现,示例如下:

public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job> {
    // 线程池最大限制数
    private static final int MAX_WORKER_NUMBERS = 10;
    
    // 线程池默认的数量
    private static final int DEFAULT_WORKER_NUMBERS = 5;
    
    // 线程池最小的数量
    private static final int MIN_WORKER_NUMBERS = 1;
    
    // 这是一个工作列表,将会向里面插入工作
    private final LinkedList<Job> jobs = new LinkedList<>();
    
    // 工作者列表
    private final LinkedList<Worker> workers = new LinkedList<>();
    
    // 线程编号生成
    private final AtomicLong threadNum = new AtomicLong();
    
    // 工作者线程的数量
    private int workerNum = DEFAULT_WORKER_NUMBERS;
    
    public DefaultThreadPool() {
        initializeWorkers(DEFAULT_WORKER_NUMBERS);
    }
    
    public DefaultThreadPool(int num) {
        workerNum = num > MAX_WORKER_NUMBERS ? MAX_WORKER_NUMBERS : Math.max(num, MIN_WORKER_NUMBERS);
        initializeWorkers(workerNum);
    }
    
    @Override
    public void execute(Job job) {
        if (job != null) {
            // 添加一个工作,然后进行通知
            synchronized (jobs) {
                jobs.addLast(job);
                jobs.notify();
            }
        }
    }
    
    @Override
    public synchronized void shutdown() {
        removeWorker(workerNum);
    }
    
    @Override
    public synchronized void addWorkers(int num) {
        // 限制新增的Worker数量不能超过最大值
        if (num + this.workerNum > MAX_WORKER_NUMBERS) {
            num = MAX_WORKER_NUMBERS - this.workerNum;
        }
        initializeWorkers(num);
        this.workerNum += num;
    }
    
    @Override
    public synchronized void removeWorker(int num) {
        if (num > this.workerNum) {
            throw new IllegalArgumentException("beyond workNum");
        }
        // 按照给定的数量停止Worker
        int count = 0;
        while (count < num) {
            workers.removeFirst().shutdown();
            count++;
        }
        this.workerNum -= count;
    }
    
    @Override
    public int getJobSize() {
        return jobs.size();
    }
    
    // 初始化线程工作者
    private void initializeWorkers(int num) {
        for (int i = 0; i < num; i++) {
            Worker worker = new Worker();
            workers.addLast(worker);
            Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.incrementAndGet());
            thread.start();
        }
    }
    
    class Worker implements Runnable {
        // 工作者,负责消费任务
        // 是否工作
        private volatile boolean running = true;
        
        @Override
        public void run() {
            while (running) {
                Job job = null;
                synchronized (jobs) {
                    // 如果工作者列表是空的,那么就等待
                    while (jobs.isEmpty()) {
                        try {
                            jobs.wait();
                        } catch (InterruptedException ex) {
                            // 感知到外部对WorkerThread的中断操作,返回
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    // 取出一个Job
                    job = jobs.removeFirst();
                }
                if (job != null) {
                    try {
                        job.run();
                    } catch (Exception ex) {
                        // 忽略Job执行中的Exception
                    }
                }
            }
        }
        
        public void shutdown() {
            running = false;
        }
    }
}

从线程池的实现可以看到,客户端调用execute(Job)方法时,会不断地向工作队列jobs中添加 Job,而每个工作者线程会不断地从jobs中取出一个 Job 并执行,当jobs为空时,工作者线程进入等待状态。

添加一个 Job 后,对工作队列jobs调用notify()方法,而不是notifyAll()方法,因为这里能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法的开销更少(避免将等待队列中的线程全部移动到阻塞队列中)。

可以看到,线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列中取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被唤醒。 

线程池实战:一个基于线程池技术的简单 Web 服务器

目前的浏览器都支持多线程访问,比如在请求一个 HTML 页面的时候,页面中包含的图片资源、样式资源会被浏览器并发地获取,这样用户就不会遇到一直等到一个图片完全下载完成才能继续查看文字内容的尴尬情况。

如果 Web 服务器是单线程的,多线程的浏览器也没有用武之地,因为服务端是一个请求一个请求地顺序处理的。因此,大部分 Web 服务器都是支持并发访问的。常用的 Java Web 服务器,如 Tomcat、Jetty,在处理请求的过程中都用到了线程池技术。

下面使用前面的线程池来构造一个简单的 Web 服务器,这个 Web 服务器用来处理 HTTP 请求,目前只能处理简单的文本和 JPG 图片内容。这个 Web 服务器使用 main 线程不断地接收客户端 Socket 的连接,将连接以及请求提交给线程池处理,从而使得 Web 服务器能够同时处理多个客户端请求。

public class SimpleHttpServer {
    // 处理HttpRequest的线程池
    static ThreadPool<HttpRequestHandler> threadPool = new DefaultThreadPool<>(1);
    
    // SimpleHttpServer的根路径
    static String basePath;
    static ServerSocket serverSocket;
    
    // 服务监听端口
    static int port = 8080;
    
    public static void setPort(int port) {
        if (port > 0) {
            SimpleHttpServer.port = port;
        }
    }
    
    public static void setBasePath(String basePath) {
        if (basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()) {
            SimpleHttpServer.basePath = basePath;
        }
    }
    
    // 启动SimpleHttpServer
    public static void start() throws Exception {
        serverSocket = new ServerSocket(port);
        Socket socket = null;
        while ((socket = serverSocket.accept()) != null) {
            // 接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行
            threadPool.execute(new HttpRequestHandler(socket));
        }
        serverSocket.close();
    }
    
    static class HttpRequestHandler implements Runnable {
        private Socket socket;
        
        public HttpRequestHandler(Socket socket) {
            this.socket = socket;
        }
        
        @Override
        public void run() {
            String line = null;
            BufferedReader br = null;
            BufferedReader reader = null;
            PrintWriter out = null;
            InputStream in = null;
            
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String header = reader.readLine();
                
                // 由相对路径计算出绝对路径
                String filePath = basePath + header.split(" ")[1];
                
                out = new PrintWriter(socket.getOutputStream());
                
                // 如果请求资源的后缀为jpg或者ico,则读取资源并输出
                if (filePath.endsWith("jpg") || filePath.endsWith("ico")) {
                    in = new FileInputStream(filePath);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int i;
                    while ((i = in.read()) != -1) {
                        baos.write(i);
                    }
                    byte[] array = baos.toByteArray();
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: Molly");
                    out.println("Content-Type: image/jpeg");
                    out.println("Content-Length: " + array.length);
                    out.println("");
                    socket.getOutputStream().write(array, 0, array.length);
                } else {
                    br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
                    out = new PrintWriter(socket.getOutputStream());
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: Molly");
                    out.println("Content-Type: text/html; charset=UTF-8");
                    out.println("");
                    while ((line = br.readLine()) != null) {
                        out.println(line);
                    }
                }
                out.flush();
            } catch (Exception ex) {
                out.println("HTTP/1.1 500 Internal Server Error");
                out.println("");
                out.flush();
            } finally {
                close(br, in, reader, out, socket);
            }
        }
        
        // 关闭流或者Socket
        private static void close(Closeable... closeables) {
            if (closeables != null) {
                for (Closeable closeable : closeables) {
                    try {
                        if (closeable != null) {
                            closeable.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

该Web服务器处理用户请求的时序图如下图所示。

SimpleHttpServer在建立了与客户端的连接之后,并不会处理客户端的请求,而是将其包装成HttpRequestHandler并交由线程池处理。在线程池中的Worker处理客户端请求的同时,SimpleHttpServer能够继续完成后续客户端连接的建立,不会阻塞后续客户端的请求。

接下来,我们通过一个测试来认识线程池技术对服务器吞吐量的提升效果。我们准备了一个简单的HTML页面,内容如下。

<html>
<head>
    <title>图片展示页面</title>
</head>
<body style="text-align:center">
    <h1>图片展示</h1>
    <h2>第一张图片</h2>
    <img src="1.jpg" style="vertical-align:middle" />
    
    <h2>第二张图片</h2>
    <img src="2.jpg" style="vertical-align:middle" />
    
    <h2>第三张图片</h2>
    <img src="3.jpg" style="vertical-align:middle" />
</body>
</html>
 

将SimpleHttpServer的根目录设定到该HTML页面所在目录,并启动SimpleHttpServer。可以访问http://localhost:8080/index.html看到相应的网页。接下来通过Apache HTTP server benchmarking tool(版本2.3)来测试不同线程数下SimpleHttpServer的吞吐量。

测试场景是5000次请求,分10个线程并发执行,测试内容主要考察响应时间(越短越好)和每秒查询的数量(越多越好),测试结果如表1-4所示。不同电脑机器实际输出可能与此表不同。

线程池线程数量​

1​

5​

10​

响应时间 /ms​

0.352​

0.246​

0.163​

每秒查询的数量​

3076​

4065​

6123​

测试完成时间 /s​

1.625​

1.230​

0.816​

​可以看到,随着线程池中线程数量的增加,SimpleHttpServer的吞吐量不断增大,响应时间不断缩短,线程池的作用非常明显。

但是,线程池的线程数量并不是越多越好,具体的数量需要通过评估每个任务的处理时间,以及当前计算机的处理器能力和数量后再确定。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加系统的开销,起到相反的作用。

博主总结:

博主之前文章从介绍多线程技术带来的好处开始,讲述了如何启动和终止线程以及线程的状态,详细阐述了多线程之间进行通信的基本方式和等待/通知的经典范式。在线程应用中,使用了等待超时、数据库连接池以及简单线程池3个不同的示例巩固Java多线程基础知识。最后通过一个简单的Web服务器将上述知识点串联起来,加深读者对知识点的理解。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值