在Java后端开发中,多线程并发问题一直是面试和实际工作中的重点和难点。本文将系统性地梳理Java中解决多线程并发问题的主要方法,并结合实际代码示例进行详细解析。
一、线程基础概念
1.1 进程与线程
线程是操作系统调度的最小任务单位,而进程是资源分配的基本单位。现代操作系统(如Windows、Linux)都采用抢占式多任务调度。
多进程 vs 多线程: • 多进程缺点:
• 创建进程开销大于创建线程(尤其在Windows系统)
• 进程间通信速度慢于线程间通信
• 多进程优点:
• 稳定性更高:一个进程崩溃不会影响其他进程
• 多线程中,任意线程崩溃可能导致整个进程崩溃
1.2 线程生命周期
线程从创建到销毁经历以下状态: graph TD A[新建状态 New] -->|start()| B[就绪状态 Runnable] B -->|获取CPU资源| C[运行状态 Running] C -->|任务完成/异常| D[死亡状态 Terminated] C -->|wait/sleep/同步锁| E[阻塞状态 Blocked] E -->|条件满足| B C -->|时间片用完| B
详细状态说明:
-
新建状态:new Thread()后,调用start()前
-
就绪状态:调用start()后,等待CPU调度
-
运行状态:获取CPU资源,执行run()方法
-
阻塞状态: • 等待阻塞:执行wait()方法
• 同步阻塞:获取synchronized锁失败
• 其他阻塞:执行sleep()或join(),等待I/O
-
死亡状态:run()方法执行完毕或发生未捕获异常
二、基础同步机制
2.1 synchronized关键字
同步方法: public synchronized void method() { // 线程安全代码 }
加锁对象为this,同一时刻只有一个线程能访问该方法。
同步代码块: public void method() { // 非同步代码
javascript
synchronized(lockObject) {
// 同步代码块
}
}
可以减小锁粒度,提高并发性能。
2.2 不需要synchronized的操作
JVM规范定义的原子操作: • 基本类型(除long和double)赋值:int n = m;
• 引用类型赋值:List list = anotherList;
注意:long和double的赋值在64位JVM中通常是原子操作,但规范未明确要求。
三、高级锁机制
3.1 Lock接口与ReentrantLock
相比于synchronized,ReentrantLock提供更灵活的锁控制: public class Counter { private final Lock lock = new ReentrantLock(); private int count;
csharp
public void add(int n) {
lock.lock(); // 获取锁
try {
count += n;
} finally {
lock.unlock(); // 确保释放锁
}
}
}
优势特性:
-
尝试获取锁:避免无限等待 if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // 操作 } finally { lock.unlock(); } }
-
可重入性:同一线程可重复获取同一把锁
-
公平锁与非公平锁:ReentrantLock(true)创建公平锁
3.2 ReadWriteLock(读写锁)
适用于读多写少的场景: public class Counter { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); private final Lock rlock = rwlock.readLock(); // 读锁 private final Lock wlock = rwlock.writeLock(); // 写锁 private int[] counts = new int[10];
csharp
// 写操作
public void inc(int index) {
wlock.lock();
try {
counts[index] += 1;
} finally {
wlock.unlock();
}
}
// 读操作
public int[] get() {
rlock.lock();
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock();
}
}
}
读写锁规则: • 读锁:允许多个线程同时持有
• 写锁:独占,持有写锁时不允许其他线程持有读锁或写锁
3.3 StampedLock(Java 8+)
改进的读写锁,支持乐观读: public class Point { private final StampedLock stampedLock = new StampedLock(); private double x, y;
ini
// 写操作
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp);
}
}
// 读操作(乐观锁)
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 乐观读
double currentX = x;
double currentY = y;
// 检查在读期间是否有写操作
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock(); // 转为悲观读
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
四、无锁编程与原子操作
4.1 原子变量(Atomic Variables)
基于CAS(Compare-And-Swap)实现的无锁线程安全操作: import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0);
csharp
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}
常用原子类: • AtomicInteger、AtomicLong:整型原子操作
• AtomicReference:引用类型原子操作
• AtomicBoolean:布尔型原子操作
五、线程协作工具
5.1 wait()与notify()/notifyAll()
经典生产者-消费者模式: public class TaskQueue { private Queue queue = new LinkedList<>();
arduino
// 消费者
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait(); // 释放锁并等待
}
return queue.remove();
}
// 生产者
public synchronized void addTask(String task) {
queue.add(task);
this.notifyAll(); // 唤醒所有等待线程
}
}
关键点:
- 必须在synchronized块中调用wait()/notify()
- wait()会释放锁,被唤醒后重新获取锁
- 使用while而不是if检查条件(避免虚假唤醒)
5.2 Semaphore(信号量)
控制同时访问特定资源的线程数量: public class AccessLimitControl { // 最多允许3个线程同时访问 private final Semaphore semaphore = new Semaphore(3);
csharp
public String access() throws InterruptedException {
semaphore.acquire(); // 获取许可
try {
// 访问受限资源
return doAccess();
} finally {
semaphore.release(); // 释放许可
}
}
}
5.3 CountDownLatch与CyclicBarrier
CountDownLatch:等待多个线程完成 public class MultiThreadCompletion { private final CountDownLatch latch = new CountDownLatch(3);
csharp
public void executeTasks() throws InterruptedException {
// 启动3个线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 执行任务
doTask();
} finally {
latch.countDown(); // 完成任务
}
}).start();
}
latch.await(); // 等待所有线程完成
System.out.println("所有任务完成");
}
}
CyclicBarrier:多个线程互相等待 public class MultiThreadSync { private final CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障点"));
scss
public void execute() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 第一阶段任务
doPhase1();
barrier.await(); // 等待其他线程
// 第二阶段任务
doPhase2();
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
六、并发容器
Java提供了线程安全的并发容器: // 并发Map ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 写时复制List(适合读多写少) CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
// 并发队列 ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
七、线程中断与守护线程
7.1 线程中断
public class InterruptExample { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断线程 t.join(); } }
class MyThread extends Thread { @Override public void run() { while (!isInterrupted()) { // 检查中断状态 // 执行任务 System.out.println("Running..."); } System.out.println("Thread interrupted"); } }
7.2 守护线程
Thread daemonThread = new Thread(() -> { while (true) { // 守护任务 } }); daemonThread.setDaemon(true); // 设置为守护线程 daemonThread.start();
特点:JVM在所有非守护线程结束后会自动退出,不等待守护线程。
八、死锁与预防
8.1 死锁条件
死锁发生的四个必要条件:
- 互斥条件
- 持有并等待
- 不可剥夺
- 循环等待
8.2 死锁示例与预防
死锁示例: // 线程1 synchronized(resourceA) { synchronized(resourceB) { // 操作 } }
// 线程2 synchronized(resourceB) { synchronized(resourceA) { // 操作 } }
预防方法:
- 固定锁顺序:所有线程按相同顺序获取锁
- 尝试获取锁:使用tryLock()设置超时
- 锁粗化:减少锁的获取/释放次数
- 使用更高级的并发工具
九、性能优化建议
- 减小锁粒度:使用同步代码块而非同步方法
- 读写分离:读多写少时使用ReadWriteLock
- 无锁编程:优先考虑原子变量和并发容器
- 避免锁竞争:使用ThreadLocal减少共享数据
- 合理使用线程池:避免频繁创建/销毁线程
十、总结
Java多线程并发编程需要综合考虑: • 正确性:确保线程安全,避免竞态条件
• 性能:减少锁竞争,提高并发度
• 可维护性:代码清晰,避免过度复杂
掌握这些多线程并发解决方案,理解Java内存模型、线程通信方式和线程生命周期,是成为优秀Java后端工程师的必备技能。在实际开发中,应根据具体场景选择合适的并发工具,在保证正确性的前提下优化性能。
参考资料: • Java官方文档:java.util.concurrent包
• 《Java并发编程实战》