Java 多线程编程详解
Java 多线程编程是实现并发任务的核心技术,通过充分利用 CPU 多核资源,可显著提升程序的执行效率(如并行处理数据、异步响应请求等)。本文从线程基础、创建方式、同步机制、线程通信到高级工具类,全面解析 Java 多线程编程的核心知识点与实战技巧。
(1)同步方法(锁是当前对象
1.
一、线程基础:什么是线程?为什么需要多线程?
在 Java 中,进程是程序的一次执行过程(如运行中的 JVM 实例),而线程是进程内的最小执行单元(一个进程可包含多个线程,共享进程资源)。多线程的核心价值在于:
- 提高资源利用率:当一个线程因 IO 操作(如读写文件、网络请求)阻塞时,其他线程可继续执行,避免 CPU 空闲。
- 提升响应速度:如 GUI 程序中,用后台线程处理数据,主线程保持界面响应(避免界面卡顿)。
线程与进程的核心区别
维度 | 进程 | 线程 |
---|---|---|
资源占用 | 独立内存空间、文件描述符 | 共享进程内存空间、资源 |
切换开销 | 大(需切换内存映射等) | 小(仅切换寄存器、栈等) |
通信方式 | 复杂(如管道、Socket) | 简单(直接读写共享变量) |
线程的生命周期
Java 线程有 5 种状态,状态转换是多线程调试的核心依据:
- 新建(New):线程对象创建后(如
new Thread()
),未调用start()
时的状态。 - 就绪(Runnable):调用
start()
后,线程进入 “可运行” 状态,等待 CPU 调度(可能处于运行中或等待调度)。 - 运行(Running):CPU 调度线程执行
run()
方法,此时处于运行状态。 - 阻塞(Blocked/Waiting/Timed Waiting):线程暂停执行,如等待锁(
Blocked
)、调用wait()
(Waiting
)、sleep(1000)
(Timed Waiting
)。 - 死亡(Terminated):
run()
方法执行完毕,或因异常终止。
二、线程创建:3 种核心方式与对比
Java 提供 3 种创建线程的方式,各有适用场景,需根据是否需要返回值、是否继承类等需求选择。
1. 继承 Thread 类(无返回值)
Thread
类是线程的核心类,继承它并重写 run()
方法(线程执行体),调用 start()
启动线程(不可直接调用 run()
,否则会作为普通方法执行)。// 1. 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() { // 线程执行体
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
// 2. 使用线程
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("线程1"); // 设置线程名
t1.start(); // 启动线程(进入就绪状态,等待CPU调度)
// 主线程执行
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
输出(顺序不确定,因 CPU 调度):
main:0
线程1:0
main:1
线程1:1
...
缺点:Java 是单继承,继承
Thread
后无法再继承其他类,灵活性低。2. 实现 Runnable 接口(无返回值,推荐)
Runnable
是函数式接口(仅 run()
方法),实现它后,将实例传入 Thread
构造器,通过 Thread
启动线程。解决了单继承限制,是最常用的方式。// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() { // 线程执行体
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
// 2. 使用线程
public class RunnableDemo {
public static void main(String[] args) {
// 创建 Runnable 实例
MyRunnable task = new MyRunnable();
// 传入 Thread 构造器,启动线程
Thread t2 = new Thread(task, "线程2"); // 直接指定线程名
t2.start();
// 主线程执行
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
优势:
- 可多线程共享一个
Runnable
实例(适合多线程处理同一份资源,如卖票系统)。 - 避免单继承限制,更灵活。
3. 实现 Callable 接口(有返回值,支持异常)
Callable
接口(Java 5+)与 Runnable
类似,但:- 有返回值(泛型
V
); - 可抛出受检异常;
- 需配合
Future
或FutureTask
获取结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1. 实现 Callable 接口(泛型指定返回值类型)
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception { // 线程执行体,有返回值
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum; // 返回 1+2+...+10 的结果
}
}
// 2. 使用线程
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建 Callable 实例
MyCallable task = new MyCallable();
// 包装为 FutureTask(实现了 Future 和 Runnable 接口)
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 传入 Thread 启动
new Thread(futureTask, "计算线程").start();
// 主线程获取结果(get() 会阻塞,直到子线程执行完毕)
System.out.println("1到10的和:" + futureTask.get()); // 输出:55
}
}
适用场景:需要线程执行结果的场景(如并行计算汇总结果)。
三、线程同步:解决并发安全问题
多线程共享资源时,若多个线程同时读写共享变量,可能导致数据不一致(如卖票系统中多线程同时减库存,出现超卖)。线程同步的核心是 “保证同一时间只有一个线程访问共享资源”。
1. 共享资源问题示例(未同步)
// 共享资源:票池
class Ticket {
private int count = 10; // 10张票
// 卖票方法(未同步)
public void sell() {
if (count > 0) {
// 模拟网络延迟(放大并发问题)
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
}
}
}
// 多线程卖票
public class UnsafeDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket(); // 共享一个票池
// 3个线程同时卖票
new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口1").start();
new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口2").start();
new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口3").start();
}
}
可能的错误输出(出现负数,超卖):
窗口1卖出1张,剩余:9
窗口2卖出1张,剩余:8
窗口3卖出1张,剩余:7
...
窗口1卖出1张,剩余:-1 // 错误!
2. synchronized 关键字(隐式锁)
synchronized
是 Java 内置的同步机制,通过 “锁” 保证代码块 / 方法的原子性(同一时间只有一个线程执行)。有两种用法:(1)同步方法(锁是当前对象 this
)
在方法声明上添加
synchronized
,锁对象为当前实例(this
)。class Ticket {
private int count = 10;
// 同步方法(锁是 this)
public synchronized void sell() {
if (count > 0) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
}
}
}
(2)同步代码块(锁是指定对象)
更灵活,可指定锁对象(如
this
、类对象、自定义对象),只同步关键代码(减少锁竞争)。class Ticket {
private int count = 10;
private Object lock = new Object(); // 自定义锁对象
public void sell() {
// 同步代码块(锁是 lock 对象)
synchronized (lock) {
if (count > 0) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
}
}
}
}
注意:
- 锁对象必须是同一个(多线程竞争同一把锁才有效)。
- 同步会降低效率(因线程阻塞等待锁),需最小化同步范围(只锁必要代码)。
3. Lock 接口(显式锁,Java 5+)
java.util.concurrent.locks.Lock
是更灵活的同步机制,需手动获取和释放锁(lock()
和 unlock()
),推荐配合 try-finally
使用(确保锁释放)。常用实现类:
ReentrantLock
(可重入锁,支持公平锁 / 非公平锁)。import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket {
private int count = 10;
// 创建锁对象(true 表示公平锁:按线程等待顺序获取锁,默认非公平锁)
private Lock lock = new ReentrantLock(true);
public void sell() {
lock.lock(); // 获取锁
try {
if (count > 0) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
}
} finally {
lock.unlock(); // 释放锁(必须在 finally 中,避免异常导致锁未释放)
}
}
}
Lock
vs synchronized
:特性 | synchronized | Lock |
---|---|---|
锁获取 / 释放 | 隐式(自动获取 / 释放) | 显式(lock() /unlock() ) |
公平锁 | 不支持 | 支持(ReentrantLock(true) ) |
中断响应 | 不支持(不可中断等待锁的线程) | 支持(lockInterruptibly() ) |
超时机制 | 不支持 | 支持(tryLock(long, TimeUnit) ) |
4. volatile 关键字(保证可见性)
volatile
用于修饰共享变量,保证多线程间的可见性(一个线程修改后,其他线程立即看到最新值),但不保证原子性(不能替代 synchronized
或 Lock
)。适用场景:单线程写、多线程读的变量(如状态标记)。
class Flag {
private volatile boolean isStop = false; // volatile 保证可见性
public void setStop(boolean stop) {
isStop = stop;
}
public void run() {
while (!isStop) { // 多线程读取时,能立即看到 isStop 的修改
System.out.println("运行中...");
}
System.out.println("已停止");
}
}
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
Flag flag = new Flag();
Thread t = new Thread(flag::run);
t.start();
// 主线程 2 秒后修改状态
Thread.sleep(2000);
flag.setStop(true);
}
}
四、线程通信:协作完成任务
多线程常需协作(如生产者生产数据后通知消费者消费),Java 提供两种核心通信方式:
1. wait()
/notify()
/notifyAll()
(基于 synchronized
)
这三个方法是
Object
类的 native 方法,必须在同步代码块 / 方法中使用(否则抛 IllegalMonitorStateException
),作用是:wait()
:释放当前锁,进入等待状态,直到被notify()
唤醒。notify()
:随机唤醒一个等待该锁的线程。notifyAll()
:唤醒所有等待该锁的线程。
生产者 - 消费者模型示例:
// 共享缓冲区
class Buffer {
private int data;
private boolean hasData = false; // 是否有数据
// 生产者放入数据
public synchronized void put(int num) throws InterruptedException {
while (hasData) { // 若有数据,等待消费者取走
wait(); // 释放锁,进入等待
}
data = num;
hasData = true;
System.out.println("生产者放入:" + num);
notify(); // 唤醒消费者
}
// 消费者取出数据
public synchronized int take() throws InterruptedException {
while (!hasData) { // 若无数据,等待生产者放入
wait(); // 释放锁,进入等待
}
hasData = false;
System.out.println("消费者取出:" + data);
notify(); // 唤醒生产者
return data;
}
}
// 生产者线程
class Producer implements Runnable {
private Buffer buffer;
// 构造器传入缓冲区
public Producer(Buffer buffer) { this.buffer = buffer; }
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
buffer.put(i);
Thread.sleep(500); // 模拟生产耗时
} catch (InterruptedException e) {}
}
}
}
// 消费者线程
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) { this.buffer = buffer; }
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
buffer.take();
Thread.sleep(1000); // 模拟消费耗时
} catch (InterruptedException e) {}
}
}
}
// 测试
public class ProducerConsumer {
public static void main(String[] args) {
Buffer buffer = new