JavaWeb 30 天入门:第十天 ——Java 多线程编程详解
今天我们将深入学习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枚举中):
- 新建状态(New):线程对象创建后,尚未调用
start()方法 - 就绪状态(Runnable):调用
start()方法后,线程等待 CPU 调度 - 运行状态(Running):CPU 调度线程,执行
run()方法 - 阻塞状态(Blocked):线程等待锁释放(如
synchronized块) - 等待状态(Waiting):线程无限期等待(如调用
wait()、join()方法) - 超时等待状态(Timed Waiting):线程在指定时间内等待(如
sleep(1000)) - 终止状态(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 的核心技术之一,主要应用场景包括:
-
Web 服务器处理请求:
- Tomcat 等服务器使用线程池处理多个客户端请求
- 每个请求由一个线程处理,避免单个请求阻塞其他请求
-
异步处理任务:
@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(); // 完成异步处理 } }); } } -
并发访问控制:
- 使用
synchronized或ReentrantLock保护共享资源(如计数器、缓存) - 使用
ConcurrentHashMap等线程安全集合
- 使用
总结与实践
知识点回顾
-
线程与进程:线程是进程内的执行单元,共享进程资源,创建销毁成本低
-
创建线程的方式:
- 继承
Thread类 - 实现
Runnable接口(推荐) - 实现
Callable接口(带返回值)
- 继承
-
线程生命周期:新建→就绪→运行→阻塞 / 等待→终止
-
线程同步:
synchronized关键字(同步方法 / 代码块)ReentrantLock可重入锁- 解决并发安全问题
-
线程通信:
wait()、notify()、notifyAll()实现线程协作 -
线程池:重用线程,提高性能,
Executors工具类创建线程池
实践任务
-
多线程售票系统:
- 创建
TicketOffice类,包含车票数量和售票方法 - 实现 4 个售票窗口线程(
TicketWindow),共享同一个TicketOffice对象 - 确保售票过程线程安全,避免超售或重复售票
- 统计每个窗口卖出的票数
- 创建
-
线程池应用:
- 使用
Executors.newFixedThreadPool(5)创建线程池 - 提交 10 个任务,每个任务计算 1-10000 的累加和
- 使用
Callable和Future获取每个任务的计算结果 - 计算所有任务结果的总和
- 使用
更多推荐



所有评论(0)