今天我们将深入学习Java 多线程编程—— 这是实现程序并发执行的关键技术。在 JavaWeb 开发中,服务器(如 Tomcat)通过多线程处理多个客户端请求,理解多线程原理和编程方式,能帮助我们编写高效、安全的 Web 应用,也是理解后续 Servlet 容器工作机制的基础。

什么是线程与进程?

在学习多线程之前,我们需要先理解两个基本概念:

  • 进程(Process):是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间、文件描述符等资源。例如:打开一个浏览器是一个进程,运行一个 Java 程序也是一个进程。

  • 线程(Thread):是进程中的一个执行单元,一个进程可以包含多个线程,多个线程共享进程的资源(内存、文件句柄等),但每个线程有自己的程序计数器、栈空间。线程是 CPU 调度的基本单位。

进程与线程的关系

  • 一个进程可以包含多个线程(至少有一个主线程)
  • 线程共享进程的资源,通信成本低
  • 进程间相互独立,资源不共享,通信成本高
  • 线程的创建和销毁成本远低于进程

多线程的优势

  • 提高 CPU 利用率:当一个线程等待 I/O 操作时,其他线程可以继续执行
  • 提高程序响应速度:例如图形界面程序中,后台线程处理数据,UI 线程保持响应
  • 简化程序设计:将复杂任务分解为多个线程并行执行

Java 中创建线程的三种方式

Java 提供了多种创建线程的方式,每种方式有其适用场景:

方式一:继承 Thread 类

java.lang.Thread是线程类,继承该类并重写run()方法即可创建线程:

// 1. 继承Thread类
class MyThread extends Thread {
    // 2. 重写run()方法,定义线程执行的任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            // Thread.currentThread()获取当前线程对象
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                // 线程休眠(单位:毫秒)
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        System.out.println("主线程开始");
        
        // 3. 创建线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // 设置线程名称
        thread1.setName("线程A");
        thread2.setName("线程B");
        
        // 4. 启动线程(调用start()方法,而非直接调用run())
        thread1.start();
        thread2.start();
        
        // 主线程执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("主线程结束");
    }
}

运行结果(可能的输出)

主线程开始
主线程:0
线程A:0
线程B:0
线程A:1
主线程:1
线程B:1
线程A:2
主线程:2
线程B:2
线程A:3
主线程:3
线程B:3
线程A:4
主线程:4
线程B:4
主线程结束

注意

  • 启动线程必须调用start()方法,而非直接调用run()(直接调用run()只是普通方法调用,不会创建新线程)
  • 线程调度由操作系统决定,因此每次运行结果可能不同(线程交替执行)

方式二:实现 Runnable 接口

java.lang.Runnable是函数式接口(只有一个run()方法),实现该接口并将其作为参数传递给Thread类:

// 1. 实现Runnable接口
class MyRunnable implements Runnable {
    // 2. 实现run()方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        System.out.println("主线程开始");
        
        // 2. 创建Runnable实现类对象
        MyRunnable runnable = new MyRunnable();
        
        // 3. 创建Thread对象,将Runnable作为参数传入
        Thread thread1 = new Thread(runnable, "线程A");
        Thread thread2 = new Thread(runnable, "线程B");
        
        // 4. 启动线程
        thread1.start();
        thread2.start();
        
        // 主线程任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("主线程结束");
    }
}

实现 Runnable 接口的优势

  • 避免单继承限制:一个类可以实现多个接口,同时继承其他类
  • 适合多个线程共享资源:多个线程可以共享同一个 Runnable 对象
  • 代码解耦:将线程任务与线程控制分离

方式三:实现 Callable 接口(带返回值)

java.util.concurrent.Callable接口与Runnable类似,但可以返回结果并抛出异常,通常与Future结合使用:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 实现Callable接口,指定返回值类型
class MyCallable implements Callable<Integer> {
    private int start;
    private int end;
    
    public MyCallable(int start, int end) {
        this.start = start;
        this.end = end;
    }
    
    // 2. 实现call()方法,有返回值
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = start; i <= end; i++) {
            sum += i;
            Thread.sleep(100);
        }
        System.out.println(Thread.currentThread().getName() + "计算完成");
        return sum; // 返回计算结果
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("主线程开始");
        
        // 2. 创建Callable实现类对象
        Callable<Integer> callable1 = new MyCallable(1, 10);
        Callable<Integer> callable2 = new MyCallable(11, 20);
        
        // 3. 创建FutureTask对象(用于接收返回值)
        FutureTask<Integer> future1 = new FutureTask<>(callable1);
        FutureTask<Integer> future2 = new FutureTask<>(callable2);
        
        // 4. 创建Thread对象并启动
        new Thread(future1, "计算线程1").start();
        new Thread(future2, "计算线程2").start();
        
        // 主线程可以做其他事情
        System.out.println("主线程处理其他任务...");
        
        // 5. 获取线程返回结果(如果线程未完成,会阻塞等待)
        int result1 = future1.get();
        int result2 = future2.get();
        
        System.out.println("1-10的和:" + result1);
        System.out.println("11-20的和:" + result2);
        System.out.println("总和:" + (result1 + result2));
        
        System.out.println("主线程结束");
    }
}

运行结果

主线程开始
主线程处理其他任务...
计算线程1计算完成
计算线程2计算完成
1-10的和:55
11-20的和:155
总和:210
主线程结束

Callable 接口的优势

  • 可以返回执行结果
  • 可以抛出受检异常
  • 适合需要获取线程执行结果的场景

线程的生命周期

线程从创建到销毁经历多个状态,Java 定义了 6 种线程状态(在Thread.State枚举中):

  1. 新建状态(New):线程对象创建后,尚未调用start()方法
  2. 就绪状态(Runnable):调用start()方法后,线程等待 CPU 调度
  3. 运行状态(Running):CPU 调度线程,执行run()方法
  4. 阻塞状态(Blocked):线程等待锁释放(如synchronized块)
  5. 等待状态(Waiting):线程无限期等待(如调用wait()join()方法)
  6. 超时等待状态(Timed Waiting):线程在指定时间内等待(如sleep(1000)
  7. 终止状态(Terminated):线程执行完毕或异常终止

状态转换图

新建(New) → 就绪(Runnable) → 运行(Running) → 终止(Terminated)
               ↑    ↓
     阻塞(Blocked) ← → 等待(Waiting)
                        ↓
                   超时等待(Timed Waiting)

线程的常用方法

1. 线程控制方法

方法 说明
start() 启动线程,使其进入就绪状态
run() 线程执行的任务,由 JVM 调用
sleep(long millis) 线程休眠指定毫秒数,进入超时等待状态
join() 等待该线程执行完毕
join(long millis) 等待该线程最多 millis 毫秒
yield() 线程让步,放弃 CPU 资源,重新进入就绪状态
interrupt() 中断线程(设置中断标志)
isInterrupted() 判断线程是否被中断
currentThread() 静态方法,返回当前执行的线程对象

join () 方法示例

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程:" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "子线程");
        
        t.start();
        
        // 主线程等待子线程执行完毕后再继续
        System.out.println("主线程等待子线程完成...");
        t.join(); // 阻塞主线程,直到子线程执行完毕
        
        System.out.println("主线程继续执行");
    }
}

2. 线程优先级

线程优先级决定了线程获得 CPU 调度的概率(优先级高的线程更可能被调度),范围从 1(最低)到 10(最高),默认优先级为 5。

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程A:" + i);
            }
        }, "线程A");
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程B:" + i);
            }
        }, "线程B");
        
        // 设置优先级
        t1.setPriority(1); // 最低优先级
        t2.setPriority(10); // 最高优先级
        
        t1.start();
        t2.start();
    }
}

注意:线程优先级只是影响调度概率,不能保证执行顺序,依赖优先级控制执行顺序是不可靠的。

3. 守护线程

守护线程(Daemon Thread)是为其他线程服务的线程,当所有非守护线程结束时,守护线程自动终止(如 JVM 的垃圾回收线程)。

public class DaemonThreadDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) { // 无限循环
                System.out.println("守护线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "守护线程");
        
        // 设置为守护线程(必须在start()前设置)
        daemonThread.setDaemon(true);
        daemonThread.start();
        
        // 主线程(非守护线程)执行5秒后结束
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束,守护线程将自动终止");
    }
}

线程同步:解决并发安全问题

当多个线程共享资源时,可能会出现并发安全问题(数据不一致)。例如:两个线程同时操作同一个银行账户的余额。

并发安全问题示例

// 银行账户类
class BankAccount {
    private int balance; // 余额
    
    public BankAccount(int balance) {
        this.balance = balance;
    }
    
    // 取款方法
    public void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + "准备取款");
            try {
                Thread.sleep(1000); // 模拟网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + "取款失败,余额不足");
        }
    }
}

public class ThreadUnsafeDemo {
    public static void main(String[] args) {
        // 两个线程共享同一个账户
        BankAccount account = new BankAccount(1000);
        
        Thread t1 = new Thread(() -> account.withdraw(800), "张三");
        Thread t2 = new Thread(() -> account.withdraw(800), "李四");
        
        t1.start();
        t2.start();
    }
}

可能的错误结果

张三准备取款
李四准备取款
张三取款成功,余额:200
李四取款成功,余额:-600 // 错误:余额变为负数

问题原因:两个线程同时进入if (balance >= amount)判断,都认为余额充足,导致最终余额为负数。

线程同步机制

Java 提供了多种线程同步机制,确保同一时刻只有一个线程访问共享资源:

1. synchronized 关键字

synchronized可以修饰方法或代码块,保证同一时刻只有一个线程执行该代码:

  • 同步方法

    // 同步方法(锁对象是this)
    public synchronized void withdraw(int amount) {
        // 方法体
    }
    
  • 同步代码块

    public void withdraw(int amount) {
        // 同步代码块(锁对象可以是任意对象,通常用this或类对象)
        synchronized (this) {
            // 需要同步的代码
        }
    }
    

修复并发安全问题

class BankAccount {
    private int balance;
    
    public BankAccount(int balance) {
        this.balance = balance;
    }
    
    // 使用同步方法
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + "准备取款");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + "取款失败,余额不足");
        }
    }
}

正确结果

synchronized的工作原理:

  • 每个对象都有一个对象锁(monitor)
  • 线程进入同步代码前需要获取锁,执行完毕后释放锁
  • 同一时刻只有一个线程能获取锁,其他线程等待
2. ReentrantLock(可重入锁)

java.util.concurrent.locks.ReentrantLock是 JDK 5 + 提供的更灵活的锁机制:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BankAccountWithLock {
    private int balance;
    // 创建锁对象
    private Lock lock = new ReentrantLock();
    
    public BankAccountWithLock(int balance) {
        this.balance = balance;
    }
    
    public void withdraw(int amount) {
        lock.lock(); // 获取锁
        try {
            // 临界区代码
            if (balance >= amount) {
                System.out.println(Thread.currentThread().getName() + "准备取款");
                Thread.sleep(1000);
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + "取款失败,余额不足");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁(必须放在finally中)
        }
    }
}

ReentrantLock相比synchronized的优势:

  • 可以尝试获取锁(tryLock()),避免死锁
  • 可以设置公平锁(先请求锁的线程先获得锁)
  • 可以通过Condition实现更灵活的等待 / 通知机制

线程通信

多个线程协作完成任务时,需要进行通信。Java 提供了wait()notify()notifyAll()等方法实现线程间通信:

  • wait():使当前线程进入等待状态,释放对象锁
  • notify():唤醒一个等待该对象锁的线程
  • notifyAll():唤醒所有等待该对象锁的线程

这些方法必须在synchronized方法或代码块中使用。

生产者 - 消费者模型示例

// 仓库类
class Warehouse {
    private int count = 0; // 产品数量
    private final int MAX = 5; // 最大容量
    
    // 生产产品
    public synchronized void produce() throws InterruptedException {
        while (count >= MAX) {
            System.out.println("仓库已满,生产者等待...");
            wait(); // 仓库满,生产者等待
        }
        count++;
        System.out.println("生产者生产,当前数量:" + count);
        notifyAll(); // 唤醒等待的消费者
    }
    
    // 消费产品
    public synchronized void consume() throws InterruptedException {
        while (count <= 0) {
            System.out.println("仓库为空,消费者等待...");
            wait(); // 仓库空,消费者等待
        }
        count--;
        System.out.println("消费者消费,当前数量:" + count);
        notifyAll(); // 唤醒等待的生产者
    }
}

// 生产者线程
class Producer implements Runnable {
    private Warehouse warehouse;
    
    public Producer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                warehouse.produce();
                Thread.sleep(500); // 生产间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private Warehouse warehouse;
    
    public Consumer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                warehouse.consume();
                Thread.sleep(1000); // 消费间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();
        
        // 创建生产者和消费者线程
        Thread producer = new Thread(new Producer(warehouse), "生产者");
        Thread consumer1 = new Thread(new Consumer(warehouse), "消费者1");
        Thread consumer2 = new Thread(new Consumer(warehouse), "消费者2");
        
        producer.start();
        consumer1.start();
        consumer2.start();
    }
}

线程池:高效管理线程资源

创建和销毁线程会消耗系统资源,线程池可以重用线程,减少资源消耗,提高响应速度。

Java 通过java.util.concurrent.Executors提供了线程池的实现:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1. 创建线程池(固定大小为3的线程池)
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 2. 提交任务(Runnable或Callable)
        for (int i = 0; i < 10; i++) {
            int taskNum = i;
            pool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务:" + taskNum);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 3. 关闭线程池
        pool.shutdown(); // 等待所有任务完成后关闭
        // pool.shutdownNow(); // 立即关闭,可能中断正在执行的任务
    }
}

常用线程池类型

  • newFixedThreadPool(n):固定大小的线程池
  • newCachedThreadPool():可缓存的线程池(根据需求自动调整大小)
  • newSingleThreadExecutor():单线程的线程池(任务串行执行)
  • newScheduledThreadPool(n):支持定时任务的线程池

多线程在 JavaWeb 中的应用

多线程是 JavaWeb 的核心技术之一,主要应用场景包括:

  1. Web 服务器处理请求

    • Tomcat 等服务器使用线程池处理多个客户端请求
    • 每个请求由一个线程处理,避免单个请求阻塞其他请求
  2. 异步处理任务

    @WebServlet("/async")
    public class AsyncServlet extends HttpServlet {
        protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
            // 开启异步处理
            AsyncContext asyncContext = request.startAsync();
            
            // 提交任务到线程池
            Executors.newCachedThreadPool().submit(() -> {
                try {
                    // 耗时操作(如数据库查询、网络请求)
                    Thread.sleep(5000);
                    asyncContext.getResponse().getWriter().write("异步处理完成");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    asyncContext.complete(); // 完成异步处理
                }
            });
        }
    }
    
  3. 并发访问控制

    • 使用synchronizedReentrantLock保护共享资源(如计数器、缓存)
    • 使用ConcurrentHashMap等线程安全集合

总结与实践

知识点回顾

  1. 线程与进程:线程是进程内的执行单元,共享进程资源,创建销毁成本低

  2. 创建线程的方式

    • 继承Thread
    • 实现Runnable接口(推荐)
    • 实现Callable接口(带返回值)
  3. 线程生命周期:新建→就绪→运行→阻塞 / 等待→终止

  4. 线程同步

    • synchronized关键字(同步方法 / 代码块)
    • ReentrantLock可重入锁
    • 解决并发安全问题
  5. 线程通信wait()notify()notifyAll()实现线程协作

  6. 线程池:重用线程,提高性能,Executors工具类创建线程池

实践任务

  1. 多线程售票系统

    • 创建TicketOffice类,包含车票数量和售票方法
    • 实现 4 个售票窗口线程(TicketWindow),共享同一个TicketOffice对象
    • 确保售票过程线程安全,避免超售或重复售票
    • 统计每个窗口卖出的票数
  2. 线程池应用

    • 使用Executors.newFixedThreadPool(5)创建线程池
    • 提交 10 个任务,每个任务计算 1-10000 的累加和
    • 使用CallableFuture获取每个任务的计算结果
    • 计算所有任务结果的总和
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐