轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

      • Java并发-线程基础与synchronized关键字
      • Java并发-重入锁ReentrantLock详解与实践
      • Java并发-信号量Semaphore
      • Java并发-读写锁ReadWriteLock
      • Java并发-倒计时器CountDownLatch
      • Java并发-栅栏CyclicBarrier
      • Java并发-LockSupport线程阻塞工具类
      • Java并发-线程池ThreadPoolExecutor
      • Java并发-阻塞队列BlockingQueue
      • Java并发-以空间换时间之ThreadLocal
      • Java并发-无锁策略CAS与atomic包
      • Java并发-JDK并发容器
      • Java并发-异步调用结果之Future和CompletableFuture
      • Java并发-Fork Join框架
      • Java并发-调试与诊断
        • 一、调试技巧与工具
          • 1、断点(Breakpoints)
          • 1.1、普通断点
          • 1.2、条件断点
          • 1.3、方法断点
          • 1.4、异常断点
          • 1.5、线程断点
          • 2、并发可视化工具
          • 2.1、线程视图
          • 2.2、监控锁与阻塞
          • 2.3、线程组视图
          • 3、步进(Stepping)
          • 3.1、基本步进操作
          • 3.2、并发调试特殊步进
          • 3.3、多线程步进策略
          • 4、变量与表达式求值
          • 4.1、变量视图
          • 4.2、表达式求值器
          • 4.3、监视窗口(Watches)
          • 5、日志和控制台输出
          • 5.1、控制台输出
          • 5.2、日志断点
        • 二、Java并发问题诊断与解决
          • 1、死锁检测与解决
          • 1.1、检测方法
          • 1.2、解决策略
          • 2、竞态条件识别与修复
          • 2.1、识别技巧
          • 2.2、修复方法
          • 3、错误的线程同步
          • 3.1、常见问题
          • 3.2、诊断方法
          • 4、线程饥饿与活锁
          • 4.1、线程饥饿
          • 4.2、活锁
          • 5、性能调优与分析
          • 5.1、性能分析工具
          • 5.2、性能优化策略
        • 三、调试实践案例
          • 1、案例1:死锁问题调试
          • 2、案例2:竞态条件调试
          • 3、案例3:线程池任务调试
          • 4、调试步骤
        • 四、并发调试最佳实践
          • 1、预防性措施
          • 1.1、代码设计原则
          • 1.2、编码规范
          • 2、调试技巧总结
          • 2.1、断点使用策略
          • 2.2、线程调试技巧
          • 2.3、性能调试建议
          • 3、常用诊断命令
          • 4、测试并发代码
          • 4.1、单元测试框架
          • 4.2、压力测试示例
        • 五、总结
    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 并发
轩辕李
2023-03-02
目录

Java并发-调试与诊断

Java并发充分利用了多核处理器的能力,提高了程序执行效率。

不过并发编程涉及多线程、同步、锁等概念,这些概念使得并发程序的设计和调试变得复杂。

调试Java并发程序具有挑战性的原因有以下几点:

  • 非确定性行为:线程间的交互可能导致不可预测的结果,相同的代码在不同运行中可能产生不同的结果
  • 时序相关问题:多线程程序可能存在死锁、竞态条件、活锁等问题
  • 难以重现:并发程序的错误可能不容易重现,使得调试变得困难
  • 观察者效应:添加调试代码可能改变程序的时序,导致问题消失或转移
  • 状态复杂性:多个线程同时修改共享状态,难以追踪状态变化

本文主要介绍使用IntelliJ IDEA来进行并发调试。

# 一、调试技巧与工具

在IntelliJ IDEA中,有许多功能强大的调试工具可以帮助我们调试Java并发程序。下面将详细介绍这些工具的使用方法。

# 1、断点(Breakpoints)

断点是调试过程中的一个关键概念。通过在代码中设置断点,可以使程序在达到断点处暂停执行,以便观察程序的运行状态。IntelliJ IDEA支持多种类型的断点。

# 1.1、普通断点

在代码行号旁边的空白区域单击鼠标左键,即可设置一个普通断点。当程序运行到这一行时,它将暂停执行。

# 1.2、条件断点

条件断点允许您在满足某个条件时暂停程序执行。右键单击已设置的普通断点,选择Edit breakpoint,在弹出的窗口中设置条件表达式。

使用场景:

  • 当循环中某个特定迭代出现问题时
  • 当某个变量达到异常值时
  • 当特定线程执行到某处时

示例:i == 100 && Thread.currentThread().getName().equals("worker-1")

# 1.3、方法断点

方法断点允许您在方法的入口和退出处暂停程序执行。在方法签名上右键单击,选择Toggle Method Breakpoint。

# 1.4、异常断点

异常断点可以在抛出特定异常时暂停程序。通过Run > View Breakpoints,点击+按钮,选择Java Exception Breakpoints。

使用场景:

  • 捕获NullPointerException、ConcurrentModificationException等并发相关异常
  • 定位异常的根源而不是处理位置

# 1.5、线程断点

线程断点只对特定线程生效。右键点击断点,在Suspend选项中选择Thread而不是All。

使用场景:

  • 调试特定工作线程的行为
  • 避免其他线程干扰调试过程

# 2、并发可视化工具

IntelliJ IDEA提供了一些可视化工具,用于监控并发程序的运行状态。

# 2.1、线程视图

线程视图显示了程序中所有活动线程的信息。在调试时,选择View > Tool Windows > Debug,在Debug窗口中,选择Threads选项卡。

线程状态说明:

  • RUNNING:正在执行
  • SLEEPING:调用了Thread.sleep()
  • WAIT:调用了Object.wait()
  • PARK:调用了LockSupport.park()
  • MONITOR:等待获取同步锁

# 2.2、监控锁与阻塞

IntelliJ IDEA可以显示锁和等待锁的线程信息。在Threads选项卡中,展开线程名称,查看锁定对象和等待锁的线程。

查看锁信息:

  • 右键点击线程,选择Get Thread Dump查看完整的线程栈和锁信息
  • 使用Analyze > Analyze Stacktrace分析线程转储

# 2.3、线程组视图

可以按线程组组织线程,便于理解线程池和线程组的结构。在Threads窗口中,点击齿轮图标,选择Customize Thread View进行配置。

# 3、步进(Stepping)

在调试过程中,可以通过以下操作控制程序的执行流程:

# 3.1、基本步进操作

  • Step Over (F8):执行当前行并移动到下一行
  • Step Into (F7):进入方法体内部
  • Step Out (Shift + F8):执行完当前方法并返回调用处
  • Smart Step Into (Shift + F7):当一行有多个方法调用时,选择进入哪个方法
  • Force Step Into (Alt + Shift + F7):强制进入任何方法,包括JDK内部方法

# 3.2、并发调试特殊步进

  • Run to Cursor (Alt + F9):运行到光标位置,适合跳过大段代码
  • Force Run to Cursor (Ctrl + Alt + F9):忽略所有断点运行到光标处
  • Drop Frame:回退到方法调用前的状态(注意:不会撤销已执行的副作用)

# 3.3、多线程步进策略

在并发调试时,可以设置步进策略:

  • 右键点击断点,在Suspend中选择:
    • All:暂停所有线程
    • Thread:仅暂停当前线程
    • 设置Make Default使其成为默认行为

# 4、变量与表达式求值

在调试过程中,您可能需要查看变量的值或计算表达式的结果。

# 4.1、变量视图

在调试时,选择View > Tool Windows > Debug,在Debug窗口中,选择Variables选项卡。

高级功能:

  • 标记对象:右键点击变量,选择Mark Object给对象添加标签,便于追踪
  • 修改变量值:右键点击变量,选择Set Value临时修改变量值
  • 添加监视:右键点击变量,选择Add to Watches持续监控变量
  • 查看对象内存地址:在变量上右键,选择View as > Object

# 4.2、表达式求值器

表达式求值器允许您在调试过程中计算任意表达式的值。点击Debug窗口中的Evaluate Expression按钮(或按Alt + F8键)。

使用技巧:

  • 可以执行方法调用:list.size()
  • 可以创建新对象:new ArrayList<>()
  • 可以修改程序状态:counter.set(0)
  • 支持Lambda表达式:list.stream().filter(x -> x > 10).count()

# 4.3、监视窗口(Watches)

监视窗口允许持续观察表达式的值变化:

  • 在Debug窗口点击+添加监视表达式
  • 支持复杂表达式如:Thread.currentThread().getName() + ": " + counter.get()

# 5、日志和控制台输出

在调试过程中,可以查看程序的日志和控制台输出,以便了解程序的运行情况。

# 5.1、控制台输出

选择View > Tool Windows > Debug,在Debug窗口中,选择Console选项卡。

控制台功能:

  • 过滤输出:使用控制台工具栏的过滤器按钮
  • 折叠重复行:点击Fold lines like this减少重复输出
  • 搜索:使用Ctrl + F在输出中搜索

# 5.2、日志断点

日志断点不会暂停程序,而是输出日志信息:

  1. 右键点击断点,取消勾选Suspend
  2. 勾选Evaluate and log
  3. 输入要记录的表达式

优势:

  • 不影响程序执行时序
  • 适合调试时序敏感的并发问题
  • 可以记录线程名、时间戳等上下文信息

# 二、Java并发问题诊断与解决

在调试Java并发程序时,可能会遇到以下问题。以下是如何识别和解决这些问题的方法。

# 1、死锁检测与解决

死锁是指两个或多个线程在等待对方释放资源的情况。

# 1.1、检测方法

  • IDE检测:IntelliJ IDEA的Threads视图会用特殊图标标记死锁线程
  • Thread Dump分析:右键线程选择Get Thread Dump,查找Found one Java-level deadlock
  • JConsole/JVisualVM:使用JDK自带工具检测死锁
  • 编程检测:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreadIds = threadMXBean.findDeadlockedThreads();
if (deadlockedThreadIds != null) {
    ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreadIds);
    // 处理死锁信息
}

# 1.2、解决策略

  • 锁顺序一致:所有线程按相同顺序获取锁
  • 使用tryLock:设置超时时间避免无限等待
  • 锁分离:将大锁拆分为多个小锁
  • 使用并发工具类:如ConcurrentHashMap代替手动同步

# 2、竞态条件识别与修复

竞态条件是指程序的行为取决于线程的相对执行顺序。

# 2.1、识别技巧

  • 条件断点:在关键位置设置条件断点,如counter != expectedValue
  • 并发压力测试:使用JCStress或ConcurrentUnit进行测试
  • 代码审查:查找共享可变状态的非同步访问

# 2.2、修复方法

  • 原子操作:使用AtomicInteger、AtomicReference等原子类
  • 同步机制:使用synchronized、ReentrantLock
  • 不可变对象:使用final字段和不可变集合
  • 线程局部存储:使用ThreadLocal避免共享

# 3、错误的线程同步

错误的线程同步可能导致死锁、性能下降或程序逻辑错误。

# 3.1、常见问题

  • 过度同步:锁粒度太大,影响并发性能
  • 同步不足:未保护共享状态,导致数据不一致
  • 锁泄漏:未正确释放锁,导致其他线程永久等待

# 3.2、诊断方法

  • 性能分析:使用Profiler查看锁竞争热点
  • 线程转储分析:查看BLOCKED状态的线程
  • 日志记录:在获取/释放锁时记录日志

# 4、线程饥饿与活锁

# 4.1、线程饥饿

症状:某些线程长时间得不到CPU时间片

检测:

  • 查看线程的WAITING或TIMED_WAITING状态持续时间
  • 监控线程的执行频率

解决:

  • 使用公平锁:new ReentrantLock(true)
  • 调整线程优先级
  • 使用ScheduledThreadPoolExecutor保证执行机会

# 4.2、活锁

症状:线程不断执行但无法取得进展

检测:

  • CPU使用率高但任务未完成
  • 线程状态频繁切换

解决:

  • 引入随机延迟避免同步冲突
  • 使用退避算法
  • 重新设计协调机制

# 5、性能调优与分析

# 5.1、性能分析工具

IntelliJ IDEA内置工具:

  • CPU Profiler:分析方法执行时间和调用频率
  • Memory Profiler:检测内存泄漏和对象分配
  • Async Profiler集成:低开销的生产环境性能分析

JDK工具:

  • JFR (Java Flight Recorder):记录详细的运行时数据
  • jstack:生成线程转储
  • jmap:生成堆转储
  • jstat:监控GC和类加载

# 5.2、性能优化策略

  • 减少锁竞争:
    • 使用读写锁ReadWriteLock
    • 使用无锁数据结构ConcurrentLinkedQueue
    • 锁分段技术
  • 优化线程池:
    • 合理设置核心线程数和最大线程数
    • 选择合适的队列类型
    • 配置拒绝策略
  • 减少上下文切换:
    • 使用合适的线程数量
    • 批量处理任务
    • 使用协程或异步编程

# 三、调试实践案例

# 1、案例1:死锁问题调试

以下代码展示了一个典型的死锁场景:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void method1() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + " acquired lock1");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " acquired lock2");
            }
        }
    }
    
    public void method2() {
        synchronized (lock2) {
            System.out.println(Thread.currentThread().getName() + " acquired lock2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " acquired lock1");
            }
        }
    }
}

调试步骤:

  1. 在两个synchronized块处设置断点
  2. 运行程序,当死锁发生时,查看Threads视图
  3. 使用Get Thread Dump查看死锁信息
  4. 修复方案:确保所有线程以相同顺序获取锁

# 2、案例2:竞态条件调试

public class RaceConditionExample {
    private int counter = 0;
    
    public void increment() {
        counter++; // 非原子操作,存在竞态条件
    }
    
    public int getCounter() {
        return counter;
    }
    
    public static void main(String[] args) throws InterruptedException {
        RaceConditionExample example = new RaceConditionExample();
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        for (int i = 0; i < 1000; i++) {
            executor.submit(example::increment);
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        System.out.println("Final counter: " + example.getCounter());
        // 期望1000,但实际可能小于1000
    }
}

调试技巧:

  1. 在counter++处设置条件断点:counter != Thread.currentThread().getId()
  2. 使用Evaluate Expression观察多个线程同时访问counter
  3. 修复方案:使用AtomicInteger或synchronized

# 3、案例3:线程池任务调试

假设我们使用ExecutorService来执行一些并发任务,任务是计算给定整数范围内的所有质数:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class PrimeFinder {
    private static final int THREAD_COUNT = 4;
    private static final int TASK_COUNT = 10;
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        List<Future<List<Integer>>> futures = new ArrayList<>();

        for (int i = 0; i < TASK_COUNT; i++) {
            int start = i * 10 + 1;
            int end = (i + 1) * 10;
            futures.add(executor.submit(new PrimeRangeFinder(start, end)));
        }

        // 获取结果时可能出现的问题
        for (Future<List<Integer>> future : futures) {
            try {
                System.out.println("Primes: " + future.get(5, TimeUnit.SECONDS));
            } catch (TimeoutException e) {
                System.err.println("Task timeout!");
                future.cancel(true);
            }
        }

        executor.shutdown();
        if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
}

class PrimeRangeFinder implements Callable<List<Integer>> {
    private final int start;
    private final int end;

    public PrimeRangeFinder(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public List<Integer> call() {
        System.out.println(Thread.currentThread().getName() + 
                         " processing range: " + start + "-" + end);
        List<Integer> primes = new ArrayList<>();
        
        for (int i = start; i <= end; i++) {
            // 检查中断状态
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Task interrupted!");
                return primes;
            }
            
            if (isPrime(i)) {
                primes.add(i);
            }
        }
        return primes;
    }

    private boolean isPrime(int number) {
        if (number <= 1) {
            return false;
        }
        for (int i = 2; i <= Math.sqrt(number); i++) {
            if (number % i == 0) {
                return false;
            }
        }
        return true;
    }
}

# 4、调试步骤

  1. 设置断点:

    • 在isPrime(int number)方法中的if (number % i == 0)这一行设置条件断点
    • 条件:number == 17 && Thread.currentThread().getName().contains("pool-1-thread-2")
  2. 监控线程状态:

    • 使用IntelliJ IDEA运行调试模式
    • 查看Threads选项卡观察线程状态
    • 可以看到多个线程正在执行PrimeRangeFinder.call()方法

    image

    线程图标说明: image

  3. 步进调试:

    • 使用Step Over (F8)逐行执行代码
    • 观察Variables选项卡查看局部变量的值
    • 使用Evaluate Expression计算表达式值

    image

  4. 继续执行:

    • 使用Continue (F9)继续执行程序
    • 在Console选项卡查看输出
    • 观察所有任务的完成情况

    image

# 四、并发调试最佳实践

# 1、预防性措施

# 1.1、代码设计原则

  • 最小化共享状态:尽量使用不可变对象和局部变量
  • 明确同步边界:清晰定义哪些代码需要同步
  • 使用高级并发工具:优先使用java.util.concurrent包的工具类
  • 避免嵌套锁:减少死锁风险

# 1.2、编码规范

// 推荐:使用try-finally确保锁释放
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();
}

// 推荐:使用volatile保证可见性
private volatile boolean flag = false;

// 推荐:使用final确保不可变性
private final List<String> list = Collections.synchronizedList(new ArrayList<>());

# 2、调试技巧总结

# 2.1、断点使用策略

  1. 分层设置断点:从高层逻辑到底层实现逐步深入
  2. 使用非暂停断点:记录日志而不影响时序
  3. 条件断点优化:只在特定条件下暂停,减少干扰

# 2.2、线程调试技巧

  1. 冻结其他线程:右键点击线程选择Freeze暂停其执行
  2. 切换当前线程:在Threads视图双击切换调试上下文
  3. 并行调试:同时观察多个线程的执行状态

# 2.3、性能调试建议

  1. 使用采样而非跟踪:减少性能开销
  2. 分段分析:逐步缩小问题范围
  3. 对比分析:比较正常和异常情况的差异

# 3、常用诊断命令

# 查看Java进程
jps -l

# 生成线程转储
jstack <pid> > thread_dump.txt

# 查看堆内存使用
jmap -heap <pid>

# 实时监控GC
jstat -gc <pid> 1000

# 启动JFR记录
jcmd <pid> JFR.start duration=60s filename=recording.jfr

# 4、测试并发代码

# 4.1、单元测试框架

  • JCStress:OpenJDK的并发测试框架
  • ConcurrentUnit:简化并发单元测试
  • Awaitility:异步操作测试

# 4.2、压力测试示例

@Test
public void testConcurrentAccess() throws InterruptedException {
    int threadCount = 100;
    CountDownLatch latch = new CountDownLatch(threadCount);
    AtomicInteger errors = new AtomicInteger(0);
    
    for (int i = 0; i < threadCount; i++) {
        new Thread(() -> {
            try {
                // 测试代码
                performOperation();
            } catch (Exception e) {
                errors.incrementAndGet();
            } finally {
                latch.countDown();
            }
        }).start();
    }
    
    assertTrue(latch.await(10, TimeUnit.SECONDS));
    assertEquals(0, errors.get());
}

# 五、总结

Java并发编程在提高程序性能的同时,也带来了调试的挑战。本文详细介绍了:

  1. 调试工具使用:掌握IntelliJ IDEA的断点、线程视图、表达式求值等功能
  2. 问题诊断方法:学会识别和解决死锁、竞态条件、线程饥饿等问题
  3. 性能优化技巧:使用性能分析工具找出瓶颈并优化
  4. 最佳实践:遵循并发编程的设计原则和编码规范

通过熟练掌握这些工具和技巧,您将能够更有效地开发和调试Java并发程序,写出高质量、高性能的并发代码。

记住,并发调试的关键在于:

  • 理解并发的本质和可能的问题
  • 选择合适的工具和方法
  • 保持耐心和细心
  • 不断实践和总结经验

祝你变得更强!

编辑 (opens new window)
#调试与诊断
上次更新: 2025/08/15
Java并发-Fork Join框架
Java8升级到Java11的实践

← Java并发-Fork Join框架 Java8升级到Java11的实践→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式