Java 多线程编程(一文讲透)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
Java 多线程编程:从基础到实践
前言:为什么需要多线程编程?
在计算机科学领域,多线程编程如同交响乐团的指挥,能够协调多个“演奏者”(线程)同时工作,从而提升程序的整体效率。对于 Java 开发者而言,掌握多线程编程是迈向高性能应用开发的重要一步。无论是处理复杂的后台任务,还是优化用户界面的响应速度,多线程技术都能为开发者提供强大的工具支持。本文将从核心概念、实现方法到实际案例,系统性地解析 Java 多线程编程的关键知识点。
一、线程与进程:理解多线程的基础
1.1 进程与线程的区别
可以将进程想象为一家工厂,而线程则是工厂内的工人。工厂(进程)是资源分配的基本单位,包含内存、文件句柄等资源;工人(线程)则是在工厂内部执行具体任务的个体。线程共享进程的资源,因此创建线程的开销比进程小得多,这也是多线程能够提升效率的原因。
1.2 多线程的优势
- 提高资源利用率:多个线程可以共享同一进程的内存空间,避免资源重复分配。
- 提升程序响应性:例如在 GUI 应用中,主线程负责界面渲染,其他线程处理耗时操作,避免界面冻结。
- 充分利用多核 CPU:现代处理器的多核特性使得并行执行线程成为可能。
二、Java 中创建线程的三种方法
2.1 方法一:继承 Thread 类
这是最直观的方式,通过继承 java.lang.Thread
类并重写 run()
方法来定义线程的行为。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getId() + " 正在执行");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
2.2 方法二:实现 Runnable 接口
这种方式解决了 Java 单继承的限制,允许对象同时继承其他类并实现线程功能。
public class Task implements Runnable {
@Override
public void run() {
System.out.println("通过 Runnable 接口实现线程逻辑");
}
public static void main(String[] args) {
Thread thread = new Thread(new Task());
thread.start();
}
}
2.3 方法三:Lambda 表达式(Java 8+)
利用 Lambda 表达式简化代码,体现函数式编程的简洁性。
public class LambdaExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda 表达式创建线程");
});
thread.start();
}
}
对比总结:
| 方法 | 优点 | 缺点 |
|---------------|-------------------------------|-------------------------------|
| 继承 Thread | 直观易懂 | 单继承限制 |
| 实现 Runnable | 解决单继承问题 | 需要额外 Thread 对象包装 |
| Lambda 表达式 | 代码简洁,适合短任务 | 需要 Java 8 及以上版本 |
三、线程的生命周期与状态转换
Java 线程的生命周期包含以下状态:
- 新建(New):通过
new Thread()
创建线程对象,但尚未调用start()
方法。 - 就绪(Runnable):调用
start()
后,线程进入就绪队列等待 CPU 时间片。 - 运行(Running):获得 CPU 资源并执行
run()
方法中的代码。 - 阻塞(Blocked):因等待 I/O 操作或锁资源而暂停。
- 等待(Waiting):调用
wait()
、join()
等方法进入等待状态。 - 终止(Terminated):
run()
方法执行完毕或因异常结束。
状态转换图示:
新建 → 就绪 → 运行 → [阻塞/等待] → 就绪 → 运行 → 终止
四、线程同步:解决并发问题的关键
4.1 为什么需要同步?
想象一个银行账户的场景:两个线程同时尝试从账户中扣除 100 元,若不进行同步,可能会出现“余额不足”的错误。这种因多个线程访问共享资源导致的不一致问题,称为竞态条件(Race Condition)。
4.2 同步工具与机制
4.2.1 synchronized 关键字
通过关键字 synchronized
可以对方法或代码块加锁,确保同一时间只有一个线程访问。
public class BankAccount {
private double balance;
public synchronized void withdraw(double amount) {
if (balance >= amount) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
balance -= amount;
}
}
}
4.2.2 ReentrantLock 类
java.util.concurrent.locks.ReentrantLock
提供更灵活的锁机制,支持尝试获取锁、超时等待等高级功能。
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
}
4.3 线程通信:wait()、notify() 和 notifyAll()
当线程需要等待某个条件满足时,可以使用 wait()
暂停自身,并通过 notify()
或 notifyAll()
唤醒其他线程。
生产者-消费者案例:
class Buffer {
private int value;
private boolean available = false;
public synchronized void put(int val) {
while (available) {
try { wait(); } catch (InterruptedException e) {}
}
this.value = val;
available = true;
notifyAll();
}
public synchronized int get() {
while (!available) {
try { wait(); } catch (InterruptedException e) {}
}
available = false;
notifyAll();
return value;
}
}
五、死锁与线程安全设计
5.1 死锁的定义与特征
死锁如同两个人互相握着手试图挣脱,却陷入僵局。Java 程序中若出现以下四要素,即可能发生死锁:
- 互斥:资源不可共享。
- 占有且等待:线程持有资源同时等待其他资源。
- 不可抢占:资源只能由持有者主动释放。
- 循环等待:线程间形成环形等待链。
5.2 避免死锁的策略
- 减少锁的粒度:缩短锁的持有时间。
- 按顺序获取锁:预先定义锁的获取顺序。
- 使用超时机制:通过
tryLock()
方法避免无限等待。
案例分析:
// 错误示例:可能导致死锁
public class DeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 holds lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1 holds lock2");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 holds lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2 holds lock1");
}
}
}).start();
}
}
六、线程池:优化资源管理的利器
6.1 线程池的作用
线程池(ExecutorService
)如同工厂的“工人调度系统”,通过复用线程减少创建和销毁的开销,同时控制并发线程数量。
6.2 常用线程池类型
类型 | 特点 |
---|---|
newFixedThreadPool(int nThreads) | 固定大小的线程池,适合任务量稳定的场景 |
newCachedThreadPool() | 线程数量动态调整,适合短生命周期任务 |
newSingleThreadExecutor() | 单线程池,确保任务顺序执行 |
newScheduledThreadPool(int corePoolSize) | 支持定时和周期性任务,如延迟执行或每秒执行一次 |
示例代码:
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
});
}
executor.shutdown();
}
}
七、高级主题:volatile 关键字与原子类
7.1 volatile 的作用
volatile
关键字确保变量的修改对所有线程可见,避免 Java 虚拟机(JVM)的指令重排序优化。但它不保证操作的原子性,例如 count++
需要配合 synchronized
或原子类。
7.2 原子类(Atomic 类)
java.util.concurrent.atomic
包中的类(如 AtomicInteger
)提供了无锁的原子操作,性能优于 synchronized
。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子性操作
}
}
八、实战案例:模拟多线程下载器
8.1 需求分析
实现一个支持分块下载的多线程下载器,利用多个线程并行下载文件片段,最后合并结果。
8.2 实现步骤
- 分割文件为多个块。
- 每个线程负责下载一个块。
- 使用线程池管理下载任务。
- 合并下载完成的块。
代码示例:
import java.io.*;
import java.net.URL;
import java.util.concurrent.*;
public class MultiThreadDownloader {
private static final int THREAD_POOL_SIZE = 4;
private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
private static final int CHUNK_SIZE = 1024 * 1024; // 1MB 分块
public static void download(String url, String outputFile) throws Exception {
URL website = new URL(url);
HttpURLConnection connection = (HttpURLConnection) website.openConnection();
int fileSize = connection.getContentLength();
int chunkCount = (fileSize + CHUNK_SIZE - 1) / CHUNK_SIZE;
RandomAccessFile file = new RandomAccessFile(outputFile, "rw");
file.setLength(fileSize);
CountDownLatch latch = new CountDownLatch(chunkCount);
for (int i = 0; i < chunkCount; i++) {
int startPos = i * CHUNK_SIZE;
int endPos = Math.min(startPos + CHUNK_SIZE - 1, fileSize - 1);
executor.execute(() -> {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
InputStream in = conn.getInputStream();
file.seek(startPos);
byte[] buffer = new byte[CHUNK_SIZE];
int read;
while ((read = in.read(buffer)) != -1) {
file.write(buffer, 0, read);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
file.close();
}
}
结论:多线程编程的实践与展望
通过本文的讲解,我们系统性地梳理了 Java 多线程编程的核心概念与实践方法。从线程创建、同步机制到线程池优化,每个知识点都通过代码示例和比喻进行了深入浅出的解析。对于初学者,建议从简单案例入手,逐步理解线程间的协作与竞争;中级开发者则可进一步探索 java.util.concurrent
包中的高级工具,如 CompletableFuture
或 ForkJoinPool
。
多线程编程既是提升程序性能的利器,也是引发复杂问题的根源。掌握其原理并遵循“同步最小化、资源有序化”的设计原则,才能真正发挥多线程的优势。希望读者通过本文的指导,能够将所学知识应用到实际项目中,开发出高效、稳定的并发程序。