Java多线程并发问题解决方案全解析

0 阅读6分钟

在Java后端开发中,多线程并发问题一直是面试和实际工作中的重点和难点。本文将系统性地梳理Java中解决多线程并发问题的主要方法,并结合实际代码示例进行详细解析。

一、线程基础概念

1.1 进程与线程

线程是操作系统调度的最小任务单位,而进程是资源分配的基本单位。现代操作系统(如Windows、Linux)都采用抢占式多任务调度。

多进程 vs 多线程: • 多进程缺点:

• 创建进程开销大于创建线程(尤其在Windows系统)

• 进程间通信速度慢于线程间通信

• 多进程优点:

• 稳定性更高:一个进程崩溃不会影响其他进程

• 多线程中,任意线程崩溃可能导致整个进程崩溃

1.2 线程生命周期

image.png 线程从创建到销毁经历以下状态: graph TD A[新建状态 New] -->|start()| B[就绪状态 Runnable] B -->|获取CPU资源| C[运行状态 Running] C -->|任务完成/异常| D[死亡状态 Terminated] C -->|wait/sleep/同步锁| E[阻塞状态 Blocked] E -->|条件满足| B C -->|时间片用完| B

详细状态说明:

  1. 新建状态:new Thread()后,调用start()前

  2. 就绪状态:调用start()后,等待CPU调度

  3. 运行状态:获取CPU资源,执行run()方法

  4. 阻塞状态: • 等待阻塞:执行wait()方法

    • 同步阻塞:获取synchronized锁失败

    • 其他阻塞:执行sleep()或join(),等待I/O

  5. 死亡状态:run()方法执行完毕或发生未捕获异常

二、基础同步机制

2.1 synchronized关键字

同步方法: public synchronized void method() { // 线程安全代码 }

加锁对象为this,同一时刻只有一个线程能访问该方法。

同步代码块: public void method() { // 非同步代码

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;

public void add(int n) {
    lock.lock();  // 获取锁
    try {
        count += n;
    } finally {
        lock.unlock();  // 确保释放锁
    }
}

}

优势特性:

  1. 尝试获取锁:避免无限等待 if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // 操作 } finally { lock.unlock(); } }

  2. 可重入性:同一线程可重复获取同一把锁

  3. 公平锁与非公平锁: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];

// 写操作
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;

// 写操作
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);

public void increment() {
    count.incrementAndGet();  // 原子操作
}

public int getCount() {
    return count.get();
}

}

常用原子类: • AtomicInteger、AtomicLong:整型原子操作

image.png • AtomicReference:引用类型原子操作

• AtomicBoolean:布尔型原子操作

五、线程协作工具

5.1 wait()与notify()/notifyAll()

经典生产者-消费者模式: public class TaskQueue { private Queue queue = new LinkedList<>();

// 消费者
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();  // 唤醒所有等待线程
}

}

关键点:

  1. 必须在synchronized块中调用wait()/notify()
  2. wait()会释放锁,被唤醒后重新获取锁
  3. 使用while而不是if检查条件(避免虚假唤醒)

image.png 5.2 Semaphore(信号量)

控制同时访问特定资源的线程数量: public class AccessLimitControl { // 最多允许3个线程同时访问 private final Semaphore semaphore = new Semaphore(3);

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);

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("所有线程到达屏障点"));

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 死锁条件

死锁发生的四个必要条件:

  1. 互斥条件
  2. 持有并等待
  3. 不可剥夺
  4. 循环等待

8.2 死锁示例与预防

死锁示例: // 线程1 synchronized(resourceA) { synchronized(resourceB) { // 操作 } }

// 线程2 synchronized(resourceB) { synchronized(resourceA) { // 操作 } }

预防方法:

  1. 固定锁顺序:所有线程按相同顺序获取锁
  2. 尝试获取锁:使用tryLock()设置超时
  3. 锁粗化:减少锁的获取/释放次数
  4. 使用更高级的并发工具

九、性能优化建议

  1. 减小锁粒度:使用同步代码块而非同步方法
  2. 读写分离:读多写少时使用ReadWriteLock
  3. 无锁编程:优先考虑原子变量和并发容器
  4. 避免锁竞争:使用ThreadLocal减少共享数据
  5. 合理使用线程池:避免频繁创建/销毁线程

十、总结

Java多线程并发编程需要综合考虑: • 正确性:确保线程安全,避免竞态条件

• 性能:减少锁竞争,提高并发度

• 可维护性:代码清晰,避免过度复杂

掌握这些多线程并发解决方案,理解Java内存模型、线程通信方式和线程生命周期,是成为优秀Java后端工程师的必备技能。在实际开发中,应根据具体场景选择合适的并发工具,在保证正确性的前提下优化性能。

参考资料: • Java官方文档:java.util.concurrent包

• 《Java并发编程实战》