如何检测由synchronized或Lock引起的线程阻塞问题

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 如何检测由synchronized或Lock引起的线程阻塞问题

背景介绍

排查问题的时候,有遇到synchronized使用不合理导致接口响应延迟,出现问题的伪代码如下:

public synchronized Object businessMethod(Object params){
    Object ret = xxxx;
    Object response = httpClient.execute(params);
    //业务逻辑
    ... ...
    return ret;
}

上面代码在访问远端http服务延迟的时候,所有访问该方法的线程都阻塞住了,最终导致了接口超时,而该场景下是不需要使用synchronized的。

由此联想到,如何检测由synchronized或java.util.concurrent.Lock引起的线程阻塞问题呢?

分析思路

从对象入手

一种思路是从对象入手,通过对象上的监视器可以获取如下信息:

  • 持有该对象锁的线程
  • 持有该对象锁的线程的重入次数
  • 正在争取该对象锁的线程们
  • 调用了wait后,等待notify的线程们

JVMTI提供了如下接口用来获取以上信息:

typedef struct {
    jthread owner;
    jint entry_count;
    jint waiter_count;
    jthread* waiters;
    jint notify_waiter_count;
    jthread* notify_waiters;
} jvmtiMonitorUsage;
//Get information about the object's monitor. 
//The fields of the jvmtiMonitorUsage structure are filled in with information about usage of the monitor.
jvmtiError GetObjectMonitorUsage(jvmtiEnv* env,jobject object,
                  jvmtiMonitorUsage* info_ptr)

但是,似乎无从下手。

从线程入手

如果多个线程在争用一把锁,那么拥有这把锁的线程就是阻塞住了多个线程的线程,然后将拥有这把锁的线程栈打印出来就可以对阻塞问题进行分析了。

所以线程信息中的下面两个信息是我们最关注的:

  • 该线程已经拥有的锁信息
  • 该线程正在争取的锁信息

JVMTI中提供了获取以上信息的接口:

Get Owned Monitor Info

jvmtiError
GetOwnedMonitorInfo(jvmtiEnv* env,
            jthread thread,
            jint* owned_monitor_count_ptr,
            jobject** owned_monitors_ptr)

Get Current Contended Monitor

jvmtiError
GetCurrentContendedMonitor(jvmtiEnv* env,
            jthread thread,
            jobject* monitor_ptr)

综上分析,该问题总的解决思路是:

  1. 获取所有线程信息
  2. 获取被争用最多的锁对象
  3. 获取每个锁对象对应的线程信息
  4. 找出拥有被争用最多的锁对象的线程信息

以上功能已经在arthas里实现了,下面看看arthas是如何实现的。

arthas: thread -b

下面是arthas thread -b命令的主要实现逻辑:

public static BlockingLockInfo findMostBlockingLock() {
    // 获取所有线程信息
    ThreadInfo[] infos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported(),
                                                     threadMXBean.isSynchronizerUsageSupported());
    // a map of <LockInfo.getIdentityHashCode, number of thread blocking on this>
    Map<Integer, Integer> blockCountPerLock = new HashMap<Integer, Integer>();
    // a map of <LockInfo.getIdentityHashCode, the thread info that holding this lock
    Map<Integer, ThreadInfo> ownerThreadPerLock = new HashMap<Integer, ThreadInfo>();
    // 通过遍历线程,获取
    // 1.被争用的锁对象,及该锁对象被多少个线程争用
    // 2.已被获取到的锁对象,及拥有该锁对象的线程
    for (ThreadInfo info: infos) {
        if (info == null) {
            continue;
        }
        LockInfo lockInfo = info.getLockInfo();
        if (lockInfo != null) {
            // the current thread is blocked waiting on some condition
            if (blockCountPerLock.get(lockInfo.getIdentityHashCode()) == null) {
                blockCountPerLock.put(lockInfo.getIdentityHashCode(), 0);
            }
            int blockedCount = blockCountPerLock.get(lockInfo.getIdentityHashCode());
            blockCountPerLock.put(lockInfo.getIdentityHashCode(), blockedCount + 1);
        }
        for (MonitorInfo monitorInfo: info.getLockedMonitors()) {
            // the object monitor currently held by this thread
            if (ownerThreadPerLock.get(monitorInfo.getIdentityHashCode()) == null) {
                ownerThreadPerLock.put(monitorInfo.getIdentityHashCode(), info);
            }
        }
        for (LockInfo lockedSync: info.getLockedSynchronizers()) {
            // the ownable synchronizer currently held by this thread
            if (ownerThreadPerLock.get(lockedSync.getIdentityHashCode()) == null) {
                ownerThreadPerLock.put(lockedSync.getIdentityHashCode(), info);
            }
        }
    }
    // find the thread that is holding the lock that blocking the largest number of threads.找出拥有【被争用最多的锁对象】的线程
    int mostBlockingLock = 0; // System.identityHashCode(null) == 0
    int maxBlockingCount = 0;
    for (Map.Entry<Integer, Integer> entry: blockCountPerLock.entrySet()) {
        if (entry.getValue() > maxBlockingCount && ownerThreadPerLock.get(entry.getKey()) != null) {
            // the lock is explicitly held by anther thread.
            maxBlockingCount = entry.getValue();
            mostBlockingLock = entry.getKey();
        }
    }
    if (mostBlockingLock == 0) {
        // nothing found
        return EMPTY_INFO;
    }
    BlockingLockInfo blockingLockInfo = new BlockingLockInfo();
    blockingLockInfo.setThreadInfo(ownerThreadPerLock.get(mostBlockingLock));
    blockingLockInfo.setLockIdentityHashCode(mostBlockingLock);
    blockingLockInfo.setBlockingThreadCount(blockCountPerLock.get(mostBlockingLock));
    return blockingLockInfo;
}

测试thread -b

下面代码模拟的是synchronized和Lock引起阻塞的场景:

  1. synchronized方式的一共起了5个线程,其中只有一个线程获取了锁,其余4个线程等待获取锁;
  2. Lock方式的一共也起了5个线程,其中只有一个线程获取了锁,其余4个线程等待获取锁;
import java.lang.management.ManagementFactory;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
    private static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
    public static void main(String[] args) {
        System.out.println(ManagementFactory.getRuntimeMXBean().getName());
        int num = 5;
        for (int i = 0; i < num; i++) {
            Thread synchronizedT = new Thread(() -> {
                synchronized (Main.class) {
                    sleep();
                }
            });
            synchronizedT.setName("synchronizedT-" + i);
            synchronizedT.start();
            Thread lockT = new Thread(() -> {
                REENTRANT_LOCK.lock();
                try {
                    sleep();
                } finally {
                    REENTRANT_LOCK.unlock();
                }
            });
            lockT.setName("lockT-" + i);
            lockT.start();
        }
    }
    public static void sleep() {
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通过thread -b期望能够显示出两个线程信息,一个是获取了锁的synchronized线程,一个是获取了锁的Lock线程,实际结果是thread -b返回了一个线程信息(从上面arthas的代码也可以得出这个结论),如图:

期望与实际不相符,这个场景下算不算arthas thread -b的一个小bug呢?

另外一点是arthas thread -b的帮助文档说明与测试结果不相符:

总结

  • synchronized关键字和java.util.concurrent.Lock用来实现线程间的同步;
  • JVMTI提供了获取对象监视器的方法:GetObjectMonitorUsage、获取线程已拥有的监视器信息的方法:GetOwnedMonitorInfo、获取线程争用的监视器的方法:GetCurrentContendedMonitor
  • 通过ManagementFactory.getThreadMXBean()可以dumpAllThreads,通过ThreadInfo可以获取LockInfo、LockedMonitors、LockedSynchronizers。
目录
相关文章
|
4月前
|
Java 开发者 C++
Java多线程同步大揭秘:synchronized与Lock的终极对决!
Java多线程同步大揭秘:synchronized与Lock的终极对决!
91 5
|
4月前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
67 0
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
46 4
|
2月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
28 2
|
3月前
|
Java
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
40 7
|
2月前
|
Java 编译器 程序员
【多线程】synchronized原理
【多线程】synchronized原理
62 0
|
4月前
|
安全 Java 开发者
Java多线程同步:synchronized与Lock的“爱恨情仇”!
Java多线程同步:synchronized与Lock的“爱恨情仇”!
90 5
|
4月前
|
Java 开发者
揭秘!为什么大神都爱用Lock接口处理线程同步?
揭秘!为什么大神都爱用Lock接口处理线程同步?
83 5
|
4月前
|
Java
在Java多线程领域,精通Lock接口是成为高手的关键。
在Java多线程领域,精通Lock接口是成为高手的关键。相较于传统的`synchronized`,Lock接口自Java 5.0起提供了更灵活的线程同步机制,包括可中断等待、超时等待及公平锁选择等高级功能。本文通过实战演练介绍Lock接口的核心实现——ReentrantLock,并演示如何使用Condition进行精确线程控制,帮助你掌握这一武林秘籍,成为Java多线程领域的盟主。示例代码展示了ReentrantLock的基本用法及Condition在生产者-消费者模式中的应用,助你提升程序效率和稳定性。
44 2
|
4月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
32 2