Java 并发编程再进阶:从实战笔记到问题攻防,7 年经验深度拆解

作为一名拥有七年 Java 开发经验的工程师,我深知并发编程是一把双刃剑 —— 用得好可以大幅提升系统性能,用不好则会引入各种难以调试的问题。本文将结合实际项目经验,深入分析并发编程中最常见的几类问题,并给出切实可行的解决方案。

一、死锁(Deadlock):系统的隐形杀手

1. 典型场景与问题表现

死锁是并发编程中最经典的问题之一,当两个或多个线程互相持有对方所需的锁,并且都在等待对方释放锁时,就会发生死锁。以下是一个典型的死锁场景:

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // 线程1:先获取lock1,再获取lock2
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 & 2...");
                }
            }
        });

        // 线程2:先获取lock2,再获取lock1
        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 2 & 1...");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

在这个例子中,线程 1 持有 lock1 并尝试获取 lock2,而线程 2 持有 lock2 并尝试获取 lock1,双方都在等待对方释放锁,从而导致死锁。

2. 解决方案

(1)保持锁的获取顺序一致

确保所有线程都按照相同的顺序获取锁,是避免死锁最有效的方法。例如,我们可以统一规定先获取 lock1 再获取 lock2:

public class DeadlockFreeSolution {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 & 2...");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 2: Holding lock 1...");
                synchronized (lock2) {
                    System.out.println("Thread 2: Holding lock 1 & 2...");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

(2)使用带超时的锁获取方法

使用ReentrantLock.tryLock(timeout)替代synchronized,在获取锁超时后释放已持有的锁:

import java.util.concurrent.locks.ReentrantLock;

public class TimeoutLockSolution {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                if (lock1.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
                    try {
                        System.out.println("Thread 1: Holding lock 1...");
                        if (lock2.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
                            try {
                                System.out.println("Thread 1: Holding lock 1 & 2...");
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        t1.start();
    }
}

二、活锁(Livelock):线程的 "忙等待" 陷阱

1. 问题表现与场景分析

活锁是另一种并发问题,线程虽然没有被阻塞,但由于不断重试相同的操作而无法继续执行。典型场景是两个线程互相响应对方的操作,导致无限循环。

以下是一个简单的活锁示例:

public class LivelockDemo {
    private static class Resource {
        private Resource owner;

        public Resource getOwner() {
            return owner;
        }

        public void setOwner(Resource owner) {
            this.owner = owner;
        }

        public synchronized boolean tryTransfer(Resource target) {
            if (owner == null) {
                owner = target;
                System.out.println(Thread.currentThread().getName() + " transferred resource to " + target);
                return true;
            }
            return false;
        }
    }

    public static void main(String[] args) {
        Resource r1 = new Resource();
        Resource r2 = new Resource();

        Thread t1 = new Thread(() -> {
            while (true) {
                if (r1.tryTransfer(r2)) break;
                try { Thread.sleep(100); } catch (InterruptedException e) {}
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            while (true) {
                if (r2.tryTransfer(r1)) break;
                try { Thread.sleep(100); } catch (InterruptedException e) {}
            }
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

在这个例子中,两个线程不断尝试将资源转移给对方,导致无限循环。

2. 解决方案

(1)引入随机延迟

在重试操作前引入随机延迟,避免线程间的同步重试:

Thread t1 = new Thread(() -> {
    Random random = new Random();
    while (true) {
        if (r1.tryTransfer(r2)) break;
        try {
            // 引入随机延迟
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {}
    }
});

(2)设置最大重试次数

限制每个线程的重试次数,超过阈值后进行降级处理:

java

int maxRetries = 100;
for (int i = 0; i < maxRetries; i++) {
    if (r1.tryTransfer(r2)) break;
    // 重试逻辑
}
// 超过最大重试次数,进行降级处理

三、线程安全集合的误用

1. 常见问题与风险

在使用线程安全集合时,开发人员常犯以下错误:

(1)认为所有操作都是原子的

虽然ConcurrentHashMap等集合是线程安全的,但复合操作(如get-then-put)仍需要额外的同步措施:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// 非原子操作,可能导致竞态条件
if (!map.containsKey("key")) {
    map.put("key", 1);
}

(2)迭代器的弱一致性问题

线程安全集合的迭代器是弱一致性的,可能无法反映最新的修改:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("element1");

// 迭代器创建后,对list的修改不会反映在迭代器中
Iterator<String> it = list.iterator();
list.add("element2");

while (it.hasNext()) {
    System.out.println(it.next()); // 只会输出element1
}

2. 解决方案

(1)使用原子操作替代复合操作

利用ConcurrentHashMap提供的原子方法:

// 使用putIfAbsent原子方法
map.putIfAbsent("key", 1);

// 使用computeIfAbsent处理复杂逻辑
map.computeIfAbsent("key", k -> expensiveCalculation(k));

(2)正确处理迭代器的弱一致性

在需要强一致性的场景下,使用显式锁:

ReentrantLock lock = new ReentrantLock();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

public void iterateWithLock() {
    lock.lock();
    try {
        // 在锁的保护下进行迭代
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            // 处理元素
        }
    } finally {
        lock.unlock();
    }
}

四、原子类的 ABA 问题

1. 问题原理与表现

原子类基于 CAS 操作,但存在 ABA 问题:当一个值从 A 变为 B 再变回 A 时,CAS 操作会认为值没有变化,但实际上已经发生了改变。

以下是一个 ABA 问题的示例:

import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {
    private static AtomicInteger atomicInt = new AtomicInteger(100);

    public static void main(String[] args) {
        Thread mainThread = new Thread(() -> {
            int expectedValue = atomicInt.get();
            System.out.println("Main thread read value: " + expectedValue);

            // 模拟其他线程修改
            Thread otherThread = new Thread(() -> {
                atomicInt.compareAndSet(100, 101);
                System.out.println("Other thread changed value from 100 to 101");
                atomicInt.compareAndSet(101, 100);
                System.out.println("Other thread changed value from 101 back to 100");
            });
            otherThread.start();
            otherThread.join();

            // 主线程尝试CAS操作
            boolean success = atomicInt.compareAndSet(expectedValue, 200);
            System.out.println("Main thread CAS operation: " + success);
        });
        mainThread.start();
    }
}

2. 解决方案

(1)使用带版本号的原子引用

AtomicStampedReference可以为值添加版本号,每次修改时版本号递增:

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolution {
    private static AtomicStampedReference<Integer> atomicRef = 
        new AtomicStampedReference<>(100, 0);

    public static void main(String[] args) {
        int[] stampHolder = new int[1];
        int expectedValue = atomicRef.get(stampHolder);
        int expectedStamp = stampHolder[0];

        // 模拟其他线程修改
        Thread otherThread = new Thread(() -> {
            int[] stampHolder1 = new int[1];
            int value1 = atomicRef.get(stampHolder1);
            int stamp1 = stampHolder1[0];
            atomicRef.compareAndSet(value1, 101, stamp1, stamp1 + 1);
            
            int[] stampHolder2 = new int[1];
            int value2 = atomicRef.get(stampHolder2);
            int stamp2 = stampHolder2[0];
            atomicRef.compareAndSet(value2, 100, stamp2, stamp2 + 1);
        });
        otherThread.start();
        otherThread.join();

        // 主线程尝试CAS操作,同时检查值和版本号
        boolean success = atomicRef.compareAndSet(expectedValue, 200, expectedStamp, expectedStamp + 1);
        System.out.println("Main thread CAS operation: " + success);
    }
}

五、锁的性能问题

1. 常见性能瓶颈

在高并发场景下,锁的不当使用会导致严重的性能问题:

(1)锁粒度太粗

使用类级锁或全局锁,导致大量线程阻塞:

public class CoarseGrainedLock {
    private final Object lock = new Object();
    private Map<String, Integer> data = new HashMap<>();

    public void update(String key, int value) {
        synchronized (lock) {
            // 整个方法都被锁住,即使只有一小部分需要同步
            data.put(key, value);
        }
    }
}

(2)锁持有时间过长

在锁的保护范围内执行耗时操作,如 IO、远程调用等:

public void process() {
    synchronized (this) {
        // 长时间的IO操作,其他线程需等待
        performIOOperation();
    }
}

2. 优化方案

(1)减小锁粒度

将大锁拆分为多个小锁,提高并发度:

public class FineGrainedLock {
    private final Map<String, Integer> data = new ConcurrentHashMap<>();
    private final Map<String, Object> locks = new ConcurrentHashMap<>();

    public void update(String key, int value) {
        // 为每个key创建单独的锁
        Object keyLock = locks.computeIfAbsent(key, k -> new Object());
        synchronized (keyLock) {
            data.put(key, value);
        }
    }
}

(2)减少锁持有时间

将不需要同步的操作移出锁的范围:

public void process() {
    // 非关键操作,无需同步
    prepareData();

    synchronized (this) {
        // 只锁住关键操作
        updateCriticalSection();
    }

    // 非关键操作,无需同步
    postProcess();
}

六、并发编程最佳实践总结

根据多年的实战经验,我总结了以下并发编程最佳实践:

  1. 优先使用 JUC 包中的工具类:避免手动实现锁机制,优先使用ConcurrentHashMap、CopyOnWriteArrayList等成熟的并发集合。
  2. 使用原子类替代简单锁:对于计数、累加等简单操作,优先使用AtomicInteger等原子类,避免使用重量级锁。
  3. 保持锁的获取顺序一致:通过统一锁的获取顺序,避免死锁的发生。
  4. 减小锁粒度:避免使用大范围的锁,将锁的范围限制到最小必要部分。
  5. 警惕 ABA 问题:在使用原子类时,注意检查是否存在 ABA 问题,必要时使用AtomicStampedReference。
  6. 进行性能测试:在高并发场景下,不同锁实现的性能差异可能很大,建议进行压测后再做选择。
  7. 使用线程池管理线程:避免手动创建和销毁线程,使用ExecutorService创建线程池,提高线程复用率。
  8. 使用工具监控并发问题:利用 JVM 工具(如 jstack、jconsole)和第三方工具(如 VisualVM)监控和诊断并发问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值