Java多线程开发实战:解锁线程安全与性能优化的关键技术
在现代Java开发中,多线程是提升程序性能、优化资源利用率的核心技术之一。无论是高并发的电商系统、实时通信应用,还是后台数据处理服务,都离不开多线程的支持。本文将从线程基础概念出发,逐步深入线程创建、常用方法、线程安全、线程池等核心技术,并通过实战案例帮助大家掌握多线程的实际应用。
一、线程与多线程基础认知
1. 什么是线程?
线程(Thread)是程序内部的一条执行流程,一个程序若只有一条执行流程,则为单线程程序。例如,简单的控制台输出程序通常是单线程执行的。
2. 多线程的定义与应用场景
多线程是指从软硬件层面实现多条执行流程的技术,多条线程由CPU调度执行。其核心价值在于提高程序执行效率,让多个任务并行处理。
典型应用场景:
- 12306购票系统:同时处理成千上万用户的查询、购票请求
- 百度网盘上传下载:后台传输文件的同时,前台可进行其他操作
- 电商平台:订单处理、库存更新、消息推送等任务并行执行
3. 并发与并行的区别
- 并发:CPU轮询调度多个线程,由于切换速度极快,给人"同时执行"的错觉(实际同一时刻只有一个线程在执行)
- 并行:同一时刻多个线程被CPU同时调度执行(需多核CPU支持)
二、Java多线程的三种创建方式
Java提供了三种主流的线程创建方式,各有优劣,适用于不同场景。
1. 继承Thread类
这是最基础的创建方式,步骤简单直接。
实现步骤:
- 定义子类继承
java.lang.Thread - 重写
run()方法,编写线程任务逻辑 - 创建子类对象,调用
start()方法启动线程
代码示例:
csharp
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new MyThread();
// 启动线程(必须调用start(),而非直接调用run())
t1.start();
// 主线程任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出:" + i);
}
}
}
// 自定义线程类
class MyThread extends Thread {
@Override
public void run() {
// 子线程任务
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
优缺点:
- 优点:编码简单,可直接使用Thread类的方法
- 缺点:Java单继承限制,线程类无法继承其他类,扩展性差
2. 实现Runnable接口
推荐使用的创建方式,规避了单继承限制,扩展性更强。
实现步骤:
- 定义任务类实现
Runnable接口 - 重写
run()方法,编写任务逻辑 - 创建任务对象,交给Thread处理
- 调用
start()方法启动线程
代码示例(含匿名内部类简化写法):
csharp
public class ThreadDemo2_2 {
public static void main(String[] args) {
// 方式1:常规写法
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(r).start();
// 方式2:匿名内部类简化
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();
// 方式3:Lambda表达式(JDK8+)
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();
// 主线程任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出:" + i);
}
}
}
优缺点:
- 优点:任务类可继承其他类、实现其他接口,扩展性强
- 缺点:无法直接返回线程执行结果
3. 实现Callable接口(JDK5+)
解决了前两种方式无法返回执行结果的问题,适用于需要获取线程执行结果的场景。
实现步骤:
- 定义任务类实现
Callable接口,指定返回值类型 - 重写
call()方法(可抛出异常),编写任务逻辑并返回结果 - 将Callable对象封装为
FutureTask(兼具Runnable和结果获取功能) - 交给Thread处理并启动线程
- 通过
FutureTask.get()方法获取执行结果
代码示例:
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) throws Exception {
// 创建Callable任务对象
Callable<String> c1 = new MyCallable(100);
Callable<String> c2 = new MyCallable(50);
// 封装为FutureTask
FutureTask<String> f1 = new FutureTask<>(c1);
FutureTask<String> f2 = new FutureTask<>(c2);
// 启动线程
new Thread(f1).start();
new Thread(f2).start();
// 获取执行结果(主线程会阻塞直到结果返回)
System.out.println(f1.get());
System.out.println(f2.get());
}
}
// 自定义Callable任务类
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "1-" + n + "的和是:" + sum;
}
}
优缺点:
- 优点:扩展性强,可获取线程执行结果
- 缺点:编码相对复杂,获取结果时可能阻塞
三、线程常用核心方法
| 方法名 | 功能说明 |
|---|---|
start() |
启动线程,JVM调用run()方法 |
run() |
线程任务逻辑所在,不可直接调用 |
getName()/setName() |
获取/设置线程名称 |
currentThread() |
获取当前执行的线程对象 |
sleep(long millis) |
让当前线程休眠指定毫秒数 |
join() |
让调用线程先执行完毕,其他线程再继续 |
关键方法示例:
- 线程休眠(sleep) :
csharp
public class ThreadApiDemo2 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("main线程输出:" + i);
try {
// 休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 线程插队(join) :
csharp
public class ThreadApiDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread2 t1 = new MyThread2();
t1.start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程输出:" + i);
if (i == 3) {
// 让t1线程插队,执行完毕后主线程再继续
t1.join();
}
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
四、线程安全问题与解决方案
1. 线程安全问题的产生条件
当满足以下三个条件时,会出现线程安全问题:
- 多个线程同时执行
- 线程共享同一个资源
- 线程对共享资源进行修改操作
模拟线程安全问题(银行取款案例):
typescript
// 账户类(共享资源)
class Account {
private String cardId;
private double money;
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money + "元");
this.money -= money;
System.out.println("余额:" + this.money + "元");
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
}
// 取款线程
class DrawThread extends Thread {
private Account account;
public DrawThread(String name, Account account) {
super(name);
this.account = account;
}
@Override
public void run() {
account.drawMoney(100000);
}
}
// 测试类
public class ThreadDemo1 {
public static void main(String[] args) {
// 同一个账户,初始余额10万
Account account = new Account("ICBC-110", 100000);
// 两个线程同时取款10万
new DrawThread("小明", account).start();
new DrawThread("小红", account).start();
}
}
问题现象:可能出现小明和小红同时取款成功,余额变为负数的情况。
2. 线程同步解决方案
核心思想:让多个线程先后依次访问共享资源,避免并发修改。
(1)同步代码块
将访问共享资源的核心代码上锁,每次只允许一个线程进入执行。
语法:
java
synchronized (锁对象) {
// 访问共享资源的核心代码
}
优化后的账户类:
kotlin
class Account {
private String cardId;
private double money;
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
// 锁对象:推荐使用共享资源本身(this)
synchronized (this) {
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money + "元");
this.money -= money;
System.out.println("余额:" + this.money + "元");
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
}
}
(2)同步方法
将整个方法上锁,简化同步代码块的写法。
语法:
arduino
修饰符 synchronized 返回值类型 方法名(参数列表) {
// 操作共享资源的代码
}
优化后的取款方法:
csharp
public synchronized void drawMoney(double money) {
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money + "元");
this.money -= money;
System.out.println("余额:" + this.money + "元");
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
底层原理:
- 实例方法:默认使用
this作为锁对象 - 静态方法:默认使用
类名.class作为锁对象
(3)Lock锁(JDK5+)
java.util.concurrent.locks.Lock接口提供了更灵活的锁定操作,推荐使用其实现类ReentrantLock。
核心方法:
lock():获取锁unlock():释放锁(建议在finally中执行,确保锁一定释放)
代码示例:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Account {
private String cardId;
private double money;
// 创建Lock锁对象
private final Lock lock = new ReentrantLock();
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
lock.lock(); // 加锁
try {
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money + "元");
this.money -= money;
System.out.println("余额:" + this.money + "元");
} else {
System.out.println(name + "取钱失败,余额不足");
}
} finally {
lock.unlock(); // 释放锁(无论是否异常,都必须释放)
}
}
}
五、线程池技术(JDK5+)
1. 线程池的核心价值
- 避免频繁创建和销毁线程的开销(线程创建成本高)
- 控制线程数量,防止线程过多导致系统资源耗尽
- 复用线程,提高程序响应速度
2. 线程池的创建方式
(1)通过ThreadPoolExecutor手动创建(推荐)
ThreadPoolExecutor是线程池的核心实现类,可灵活配置参数。
构造器参数说明:
arduino
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻线程)
int maximumPoolSize, // 最大线程数(核心+临时线程)
long keepAliveTime, // 临时线程空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(创建线程)
RejectedExecutionHandler handler // 任务拒绝策略
)
任务拒绝策略:
| 策略 | 说明 |
|---|---|
| AbortPolicy | 丢弃任务并抛出异常(默认) |
| DiscardPolicy | 丢弃任务,不抛出异常 |
| DiscardOldestPolicy | 丢弃队列中最久的任务,加入新任务 |
| CallerRunsPolicy | 由提交任务的线程(如主线程)直接执行 |
代码示例:
java
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
// 创建线程池
ExecutorService pool = new ThreadPoolExecutor(
3, // 核心线程数3
5, // 最大线程数5
10, // 临时线程空闲10秒后销毁
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), // 任务队列容量3
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
);
// 提交任务(Runnable)
Runnable task = new MyRunnable();
for (int i = 0; i < 9; i++) {
pool.execute(task);
}
// 关闭线程池(一般不主动关闭,除非程序结束)
// pool.shutdown(); // 等待所有任务执行完毕后关闭
}
}
(2)通过Executors工具类创建(不推荐在大型系统中使用)
Executors提供了简化的线程池创建方法,但存在资源耗尽风险:
newFixedThreadPool(int n):固定线程数的线程池newSingleThreadExecutor():单线程线程池newCachedThreadPool():缓存线程池(线程数可无限增长)newScheduledThreadPool(int corePoolSize):定时任务线程池
风险说明(阿里巴巴Java开发手册):
FixedThreadPool和SingleThreadExecutor:任务队列长度为Integer.MAX_VALUE,可能堆积大量任务导致OOMCachedThreadPool和ScheduledThreadPool:最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM
六、实战案例:红包雨游戏
需求说明
- 100名员工(100个线程)抢200个红包
- 小红包(1-30元)占80%(160个),大红包(31-100元)占20%(40个)
- 输出抢红包过程,活动结束后按员工抢到的总金额降序排序
核心思路
- 生成符合要求的红包集合(线程共享资源)
- 100个线程并发抢红包,通过同步机制保证线程安全
- 统计每个员工抢到的总金额,排序后展示
完整代码
csharp
import java.util.*;
// 红包抢夺线程
class PeopleGetRedPacket extends Thread {
private List<Integer> redPackets;
private static Map<String, Integer> totalMoneyMap = new HashMap<>(); // 统计总金额
public PeopleGetRedPacket(List<Integer> redPackets, String name) {
super(name);
this.redPackets = redPackets;
totalMoneyMap.put(name, 0); // 初始化总金额为0
}
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true) {
synchronized (redPackets) {
if (redPackets.isEmpty()) {
break;
}
// 随机抢夺一个红包
int index = new Random().nextInt(redPackets.size());
Integer money = redPackets.remove(index);
System.out.println(name + "抢到红包:" + money + "元");
// 更新总金额
totalMoneyMap.put(name, totalMoneyMap.get(name) + money);
if (redPackets.isEmpty()) {
System.out.println("\n红包雨结束!");
// 排序并展示结果
showResult();
break;
}
}
}
}
// 展示抢红包结果(按总金额降序)
private void showResult() {
System.out.println("=== 抢红包结果排名 ===");
// 将map转换为list并排序
List<Map.Entry<String, Integer>> list = new ArrayList<>(totalMoneyMap.entrySet());
list.sort((o1, o2) -> o2.getValue() - o1.getValue());
// 输出排名
for (int i = 0; i < list.size(); i++) {
Map.Entry<String, Integer> entry = list.get(i);
System.out.println((i + 1) + "、" + entry.getKey() + " 总金额:" + entry.getValue() + "元");
}
}
}
// 测试类
public class ThreadTest {
public static void main(String[] args) {
// 生成200个红包
List<Integer> redPackets = getRedPackets();
// 创建100个员工线程抢红包
for (int i = 1; i <= 100; i++) {
new PeopleGetRedPacket(redPackets, "员工" + i).start();
}
}
// 生成红包:160个小红包,40个大红包
private static List<Integer> getRedPackets() {
List<Integer> redPackets = new ArrayList<>();
Random random = new Random();
// 小红包(1-30元)
for (int i = 0; i < 160; i++) {
redPackets.add(random.nextInt(30) + 1);
}
// 大红包(31-100元)
for (int i = 0; i < 40; i++) {
redPackets.add(random.nextInt(70) + 31);
}
return redPackets;
}
}
七、总结
Java多线程技术是一把"双刃剑":合理使用能大幅提升程序性能,但如果处理不当(如线程安全问题、资源耗尽),会导致程序异常甚至崩溃。
核心要点回顾:
- 线程创建优先选择
Runnable或Callable接口,规避单继承限制 - 线程安全问题需通过同步代码块、同步方法或Lock锁解决
- 线程池推荐使用
ThreadPoolExecutor手动创建,明确配置参数 - 高并发场景需注意资源控制,避免OOM等风险
掌握多线程技术需要不断实践,在实际开发中需根据业务场景选择合适的技术方案,平衡性能与安全性。