Java 多线程编程:提升系统并发处理能力!
多线程是 Java 中实现并发任务执行的关键技术,能够显著提升程序在多核处理器上的性能以及处理多任务的能力。本文面向初级到中级开发者,从多线程的基本定义开始,逐步讲解线程创建、状态管理、同步机制、并发工具以及新兴的虚拟线程技术。每部分都配有详细的代码示例,帮助读者理解并实践 Java 多线程编程。
什么是多线程?
多线程是指在同一程序中同时运行多个执行流,每个执行流称为一个线程。在 Java 中,线程由 Thread
类或其子类表示,每个线程独立执行特定的任务。通过 Thread.currentThread()
方法,可以获取当前正在运行的线程对象及其信息,例如线程名称。多线程的主要优势在于充分利用 CPU 的多核特性,通过并行执行任务提高程序的吞吐量和响应速度。例如,在 Web 服务器中,每个用户请求可以分配一个线程处理,从而实现高并发的请求处理能力。
创建多线程的三种基本方式
Java 提供了多种创建线程的方式,每种方法适用于不同的场景。以下是三种常见实现及其代码示例:
-
继承
Thread
类通过继承
Thread
类并重写run()
方法,可以直接定义线程的行为。调用start()
方法启动线程后,JVM 会执行run()
中的逻辑。public class ThreadDemo { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); } } class MyThread extends Thread { public void run() { System.out.println("Thread running: " + Thread.currentThread().getName()); } }
-
实现
Runnable
接口实现
Runnable
接口并将其传递给Thread
对象,是一种更灵活的方式。这种方法将任务逻辑与线程对象分离,符合接口优先的设计原则,常用于需要复用任务逻辑的场景。public class ThreadDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new MyRunnable()); thread.start(); } } class MyRunnable implements Runnable { public void run() { System.out.println("Runnable running: " + Thread.currentThread().getName()); } }
-
使用
Callable
和Future
Callable
接口允许线程执行完成后返回结果,结合Future
对象可以获取异步任务的返回值。通常与线程池搭配使用,适用于需要计算结果并进一步处理的场景。public class ThreadDemo { public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(new MyCallable()); System.out.println(future.get()); // 阻塞获取结果 executor.shutdown(); } } class MyCallable implements Callable<String> { public String call() { return "Result from thread: " + Thread.currentThread().getName(); } }
对于初学者,建议从 Runnable
开始学习,因其简单且易于扩展。中级开发者可以探索 Callable
,结合线程池实现更复杂的异步任务。
线程的状态与管理
Java 中的线程在其生命周期中会经历多种状态,这些状态由 JVM 管理并反映线程的当前执行情况:
- 新建(New):线程对象已创建,但未调用
start()
方法。 - 运行(Runnable):调用
start()
后,线程进入可运行状态,等待 CPU 调度。 - 阻塞(Blocked):线程尝试获取锁但被其他线程占用,进入阻塞队列。
- 等待(Waiting):通过
wait()
、join()
等方法进入等待状态,需其他线程唤醒。 - 超时等待(Timed Waiting):通过
sleep()
或wait(timeout)
设置有限等待时间。 - 终止(Terminated):线程执行完毕或异常退出。
线程管理涉及以下关键操作:
- 中断:调用
thread.interrupt()
发送中断信号,线程可通过isInterrupted()
检查状态并响应。通常用于优雅地终止线程。 - 守护线程:调用
thread.setDaemon(true)
将线程标记为守护线程。当所有非守护线程结束时,JVM 会退出,无论守护线程是否完成。常用于日志记录或监控等后台任务。
线程同步与锁机制
当多个线程同时访问共享资源时,可能导致数据不一致或竞争条件。Java 提供了多种锁机制来确保线程安全,每种机制适用于不同的并发场景。以下通过一个示例学校布告栏详细展示这些机制。
示例场景
在 Java 多线程环境中,多个学生(读线程)同时查看布告栏,而一个老师(写线程)更新布告栏,这种场景涉及读写并发问题。下面介绍几种 Java 常见的同步机制,并给出相应的代码示例。
1. synchronized
(独读独写,不可中断)
synchronized
是 Java 中最基本的同步机制,通过锁住对象或代码块,确保同一时间只有一个线程可以访问受保护的资源。
原理: synchronized
使用对象锁来实现同步。每个对象都有一个与之关联的锁,当线程进入 synchronized
代码块或方法时,它会尝试获取对象的锁。如果获取成功,线程将继续执行;否则,线程将被阻塞,直到其他线程释放锁。
使用场景
老师写布告栏时,学生不能读且排队等待获取锁;学生读取时,老师不能写且排队等待获取锁。
代码示例
class BulletinBoard {
private String message = "Initial Message";
// 读方法:只有一个线程读取
public synchronized String read() {
return message;
}
// 写方法:只有一个线程可以写
public synchronized void write(String newMessage) {
System.out.println(Thread.currentThread().getName() + " 正在写入布告栏...");
message = newMessage;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + message);
}
}
public class SynchronizedExample {
public static void main(String[] args) {
BulletinBoard board = new BulletinBoard();
// 启动多个学生线程(读)
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 读取到消息: " + board.read());
}, "学生" + i).start();
}
// 启动老师线程(写)
new Thread(() -> {
board.write("New Announcement");
}, "老师").start();
}
}
特点
synchronized
适合简单场景,但读写都需要加锁,会影响读性能(读操作不能并发)。synchronized
简单但不够灵活,读和写操作互斥,学生之间也不能并发读,效率较低。
2. ReentrantLock
(独读独写,可中断)
ReentrantLock
是一个可重入锁,它提供比 synchronized
更灵活的锁机制,比如尝试锁、超时等待、可中断锁等。
原理: ReentrantLock
实现了 Lock
接口,提供了 lock()
和 unlock()
方法来获取和释放锁。它支持可重入性,即同一个线程可以多次获取同一个锁。
使用场景
老师写布告栏时锁定,学生读时也需要锁定,但可以用更细粒度的控制,如尝试获取锁。
代码示例
import java.util.concurrent.locks.ReentrantLock;
class BulletinBoardLock {
private String message = "Initial Message";
private final ReentrantLock lock = new ReentrantLock(); // 创建重入锁
public String read() {
lock.lock(); // 加锁
try {
return message;
} finally {
lock.unlock(); // 释放锁
}
}
public void write(String newMessage) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入布告栏...");
message = newMessage;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + message);
} finally {
lock.unlock();
}
}
}
public class ReentrantLockExample {
public static void main(String[] args) {
BulletinBoardLock board = new BulletinBoardLock();
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(Thread.currentThread().getName() + " 读取到消息: " + board.read()), "学生" + i).start();
}
new Thread(() -> board.write("Updated Announcement"), "老师").start();
}
}
特点
ReentrantLock
需要手动加锁、解锁,容易出错。仍然是独占锁,读操作不能并发,影响性能。- ReentrantLock 比 synchronized 更灵活,但仍未解决读读并发问题,需要手动管理锁。
3. ReentrantReadWriteLock(多读独写)
ReadWriteLock
提供读锁和写锁,允许多个读线程并发访问,但写线程独占资源。适合读多写少的场景。
原理:ReadWriteLock
维护一对锁:读锁和写锁。读锁允许多个线程同时获取,写锁只允许一个线程获取。当线程获取读锁时,其他线程可以继续获取读锁,但不能获取写锁;当线程获取写锁时,其他线程都不能获取读锁或写锁。
使用场景
多个学生可以同时读布告栏,但老师不能写;老师写时,所有学生不能读。
代码示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
class BulletinBoardRW {
private String message = "Initial Message";
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作(多个线程可以并发执行)
public String read() {
rwLock.readLock().lock();
try {
return message;
} finally {
rwLock.readLock().unlock();
}
}
// 写操作(只有一个线程能执行)
public void write(String newMessage) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入布告栏...");
message = newMessage;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + message);
} finally {
rwLock.writeLock().unlock();
}
}
}
public class ReadWriteLockExample {
public static void main(String[] args) {
BulletinBoardRW board = new BulletinBoardRW();
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(Thread.currentThread().getName() + " 读取到消息: " + board.read()), "学生" + i).start();
}
new Thread(() -> board.write("Updated Announcement"), "老师").start();
}
}
特点
- 允许多个读线程并发,提高读性能。但写操作会阻塞所有读操作。
- ReadWriteLock 允许多个学生并发读,提高效率,但写操作仍需独占。
4. StampedLock
(乐观读独写)
StampedLock
是 Java 8 引入的优化读写锁,支持乐观读(无锁读)性能更高,但使用更复杂。
原理: StampedLock
维护一个邮戳(stamp),用于表示锁的状态。线程在读取共享资源时,可以先尝试获取乐观读邮戳,如果验证通过,则读取成功;否则,线程需要获取悲观读锁。
使用场景
多个学生可以乐观读布告栏(假设内容不常变),老师写时仍需锁定。
代码示例
import java.util.concurrent.locks.StampedLock;
class BulletinBoardStamped {
private String message = "Initial Message";
private final StampedLock lock = new StampedLock();
public String read() {
long stamp = lock.tryOptimisticRead();
String result = message;
if (!lock.validate(stamp)) { // 检查是否有写入发生
stamp = lock.readLock();
try {
result = message;
} finally {
lock.unlockRead(stamp);
}
}
return result;
}
public void write(String newMessage) {
long stamp = lock.writeLock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入布告栏...");
message = newMessage;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + message);
} finally {
lock.unlockWrite(stamp);
}
}
}
特点
- 乐观读锁在没有写入的情况下提高读取效率。
- 适用于读多写少的场景。
5. volatile
(保证单一变量的可见性)
volatile
是 Java 中的关键字,用于保证单一变量的可见性。当一个线程修改了被 volatile
修饰的变量的值,其他线程能够立即看到最新的值。
volatile
关键字会强制线程在修改共享变量后立即将值刷新到主内存,并在其他线程读取该变量时强制它们从主内存读取最新的值。
volatile 确保变量的可见性,但不提供互斥性。适用于只有一个线程写,多个线程读的场景。
使用场景
老师更新布告栏内容,学生直接读取最新内容,无需锁。
class BulletinBoardVolatile {
private volatile String content = "初始内容"; // volatile 保证可见性
public void write(String newContent) {
System.out.println("[老师] 开始更新(volatile)...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
content = newContent;
System.out.println("[老师] 已更新内容:" + content);
}
public String read() {
System.out.println(Thread.currentThread().getName() + " 读取内容(volatile):" + content);
return content;
}
}
public class VolatileExample{
public static void main(String[] args) {
BulletinBoard board = new BulletinBoard();
new Thread(() -> board.write("讲座通知"), "Teacher").start();
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
while (true) { // 持续监听变化
board.read();
try { Thread.sleep(300); } catch (InterruptedException e) {}
}
}, "Student-"+i).start();
}
}
}
特点
- 适用于简单变量的同步,但不能保证原子性(多个变量或复杂逻辑需要加锁)。
6. Semaphore
(控制访问数量)
如果需要限制同时访问的读线程数,可以使用 Semaphore
。
Semaphore 是信号量,用于控制同时访问资源的线程数量。
使用场景
限制同时读布告栏的学生数量(如最多 5个学生),老师写时独占。
import java.util.concurrent.Semaphore;
class BulletinBoardSemaphore {
private String content = "初始内容";
private final Semaphore semaphore = new Semaphore(3); // 允许3个并发读
public void write(String newContent) throws InterruptedException {
semaphore.acquire(3); // 获取所有许可
try {
System.out.println("[老师] 开始更新(Semaphore)...");
Thread.sleep(1000);
content = newContent;
System.out.println("[老师] 已更新内容:" + content);
} finally {
semaphore.release(3);
}
}
public String read() throws InterruptedException {
semaphore.acquire();
try {
System.out.println(Thread.currentThread().getName() + " 开始读取(Semaphore)...");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 读取内容:" + content);
return content;
} finally {
semaphore.release();
}
}
}
public class SemaphoreExpamle{
public static void main(String[] args) {
BulletinBoardSemaphore board = new BulletinBoardSemaphore();
new Thread(() -> {
try { board.write("考试注意事项"); }
catch (InterruptedException e) { e.printStackTrace(); }
}, "Teacher").start();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try { board.read(); }
catch (InterruptedException e) { e.printStackTrace(); }
}, "Student-"+i).start();
}
}
}
特点
- 适用于限制并发访问数的场景。
总结
同步机制 | 读性能 | 写性能 | 特点 |
---|---|---|---|
synchronized | 低 | 低 | 简单易用,自动释放锁,但无法中断等待 |
ReentrantLock | 低 | 低 | 可中断、可设置超时,需要手动释放锁 |
ReadWriteLock | 高 | 低 | 读写分离,提高读多写少场景的性能 |
StampedLock | 最高 | 低 | 支持乐观读,减少锁竞争,适合读远多于写的场景 |
volatile | 最高 | 高 | 仅保证可见性,不保证原子性,适合简单状态标志 |
Semaphore | 可控 | 低 | 控制并发线程数量,适合资源池或限流场景 |
选择合适的机制可以大幅提升系统的并发性能和可扩展性!
并发发集合 concurrent
在多线程环境下,普通的集合类(如 ArrayList
、HashMap
等)不是线程安全的。如果多个线程同时修改这些集合,可能会导致数据不一致或抛出 ConcurrentModificationException
异常。为了解决这个问题,Java 提供了 java.util.concurrent
包,其中包含了一系列线程安全的并发集合类。。以下是常见接口及其实现对比:
接口 | 非线程安全 | 线程安全 |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | LinkedList | LinkedBlockingQueue |
ConcurrentHashMap
是高并发场景下的优选实现,通过分段锁和无锁读取优化了性能,适合需要频繁读写的应用。
import java.util.concurrent.*;
public class ConcurrentCollectionsExample {
public static void main(String[] args) throws InterruptedException {
// ConcurrentHashMap 示例
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("A", 1);
concurrentHashMap.put("B", 2);
System.out.println("ConcurrentHashMap: " + concurrentHashMap);
// CopyOnWriteArrayList 示例
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("X");
copyOnWriteArrayList.add("Y");
System.out.println("CopyOnWriteArrayList: " + copyOnWriteArrayList);
// LinkedBlockingQueue 示例
LinkedBlockingQueue<Integer> linkedBlockingQueue = new LinkedBlockingQueue<>();
linkedBlockingQueue.put(10);
linkedBlockingQueue.put(20);
System.out.println("LinkedBlockingQueue: " + linkedBlockingQueue);
// 创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 使用 ConcurrentHashMap 的示例
executorService.submit(() -> {
concurrentHashMap.put("C", 3);
System.out.println("ConcurrentHashMap (Thread 1): " + concurrentHashMap);
});
executorService.submit(() -> {
concurrentHashMap.get("A");
System.out.println("ConcurrentHashMap (Thread 2): " + concurrentHashMap);
});
// 使用 CopyOnWriteArrayList 的示例
executorService.submit(() -> {
copyOnWriteArrayList.add("Z");
System.out.println("CopyOnWriteArrayList (Thread 3): " + copyOnWriteArrayList);
});
// 关闭线程池
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
// 输出最终结果
System.out.println("Final ConcurrentHashMap: " + concurrentHashMap);
System.out.println("Final CopyOnWriteArrayList: " + copyOnWriteArrayList);
}
}
创建多线程的第四种方式:线程池与 ExecutorService
线程池是一种管理线程的工具,通过复用线程避免频繁创建和销毁的开销,提高资源利用率。ExecutorService
是 Java 提供的线程池接口,支持任务提交、执行管理和结果获取。常见的线程池类型包括固定线程池(newFixedThreadPool
)、单线程池(newSingleThreadExecutor
)和缓存线程池(newCachedThreadPool
)。
以下是一个扩展示例,模拟订单处理并统计完成任务数:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolDemo {
// 用于统计任务完成次数的共享计数器,线程安全
private static final AtomicInteger completedTasks = new AtomicInteger(0);
// 定义订单处理任务
static class OrderTask implements Runnable {
private final int orderId;
public OrderTask(int orderId) {
this.orderId = orderId;
}
@Override
public void run() {
try {
// 模拟订单处理耗时 1 秒
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " processed order " + orderId);
completedTasks.incrementAndGet(); // 任务完成计数加 1
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 主方法,演示线程池用法
public static void main(String[] args) throws InterruptedException {
// 创建固定大小的线程池,最多 3 个线程
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交 5 个订单任务
for (int i = 1; i <= 5; i++) {
executor.submit(new OrderTask(i));
}
// 关闭线程池,不接受新任务
executor.shutdown();
// 等待所有任务完成,最多等待 10 秒
boolean finished = executor.awaitTermination(10, TimeUnit.SECONDS);
if (finished) {
System.out.println("All tasks completed. Total: " + completedTasks.get());
} else {
System.out.println("Timeout! Tasks completed: " + completedTasks.get());
}
}
}
结果获取方式:
-
使用
Future
:适用于需要同步获取结果的场景。Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; }); System.out.println("Result: " + future.get());
-
使用
CompletableFuture
:支持异步回调,适合复杂任务流。CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (Exception e) {} return 42; }).thenAccept(result -> System.out.println("Result: " + result));
ThreadLocal
:线程隔离
ThreadLocal
是一个线程局部存储工具,为每个线程提供独立的变量副本,避免线程间的数据干扰。它通过维护一个线程到对象映射表实现隔离,常用于 Web 应用中保存用户会话信息或数据库连接等线程私有数据。需要注意,在使用完毕后调用 remove()
方法清理数据,以防止内存泄漏。
以下是一个扩展示例,模拟 Web 请求中的用户上下文管理:
public class ThreadLocalDemo {
// 定义 ThreadLocal,存储每个线程的用户 ID
private static final ThreadLocal<String> userId = new ThreadLocal<>();
// 模拟 Web 请求处理的任务
static class RequestTask implements Runnable {
private final String id;
public RequestTask(String id) {
this.id = id;
}
@Override
public void run() {
// 设置当前线程的用户 ID
userId.set(id);
try {
// 模拟处理请求
System.out.println(Thread.currentThread().getName() + " handling user: " + userId.get());
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 清理 ThreadLocal,避免内存泄漏
userId.remove();
}
}
}
// 主方法,演示 ThreadLocal 用法
public static void main(String[] args) throws InterruptedException {
// 创建线程池,模拟多个请求
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交两个用户请求
executor.submit(new RequestTask("User1"));
executor.submit(new RequestTask("User2"));
// 关闭线程池并等待
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}
虚拟线程(Project Loom)
虚拟线程是 Java 19 引入的实验性特性(Project Loom),旨在解决传统线程在高并发场景下的资源开销问题。传统线程(平台线程)直接映射到操作系统线程,创建和维护成本高,难以支持大规模并发。而虚拟线程由 JVM 管理,作为轻量级执行单元,依托少量的载体线程运行,适合 IO 密集型任务,例如处理大量网络请求。使用虚拟线程需要启用 --enable-preview
编译和运行参数。
以下是一个扩展示例,模拟高并发请求处理:
import java.util.concurrent.*;
public class VirtualThreadDemo {
// 定义任务,模拟处理请求
static class RequestTask implements Runnable {
private final int requestId;
public RequestTask(int requestId) {
this.requestId = requestId;
}
@Override
public void run() {
try {
// 模拟 IO 操作(如网络请求)
Thread.sleep(1000);
System.out.println(Thread.currentThread() + " processed request " + requestId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 主方法,演示虚拟线程
public static void main(String[] args) throws InterruptedException {
// 使用虚拟线程执行器,每个任务一个虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交 100 个请求任务
for (int i = 1; i <= 100; i++) {
executor.submit(new RequestTask(i));
}
// 等待 2 秒观察结果
Thread.sleep(2000);
} // try-with-resources 自动关闭执行器
System.out.println("All tasks submitted");
}
}
总结
- 初级阶段:学习
Runnable
和synchronized
,掌握线程创建和基本同步。 - 中级阶段:深入理解锁机制、线程池和并发集合,探索
CompletableFuture
的异步编程。 - 进阶方向:关注虚拟线程技术,适应高并发场景的需求。