首页 > 其他分享 >记一次锁使用不当导致Dubbo线程阻塞问题

记一次锁使用不当导致Dubbo线程阻塞问题

时间:2024-06-10 16:15:27浏览次数:19  
标签:Dubbo java 使用不当 util concurrent writeLock 线程 ThreadPoolExecutor

背景

线上环境一个后台项目,提供基于dubbo实现的事件分发服务,最近突然出现心跳超时。

问题分析

检查内存是否溢出

jstat -gcutil 8166 1000

意料之中,内存正常,因为内部有接入内存溢出告警,如果是内存溢出应该有收到通知,但是这次没有溢出通知。

查看线程栈

jstack -l 8166

发现有大量DubboServerHandler开头的线程阻塞在一个同样的地方,脱敏简化后信息如下:

"DubboServerHandler-192.168.160.42:9184-thread-200" #372 daemon prio=5 os_prio=0 tid=0x0000000002342000 nid=0x252b waiting on condition [0x00007f2ce8deb000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000008a51c930> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
	at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    at com.▇▇▇▇▇▇.QueueMiddleComponent.save(QueueMiddleComponent.java:317)
	...
	at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:81)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

可以看到线程正在申请加锁,找到相关人员确认了这个不是业务代码而是我们自己内部封装的一个框架逻辑,于是找到对应的项目代码拉到本地。

找到对应的执行代码段

final ReentrantLock writeLock = this.writeLock;
writeLock.lock();
WriteBatch wb = createWriteBatch();
try {
    //业务代码
} finally {
    try {
        wb.close();
        writeLock.unlock();
    } catch (IOException e) {
        writeLock.unlock();
        throw new IllegalStateException(e.getMessage());
    }
}

加锁的代码就是writeLock.lock();这一句,而当前对象的writeLock是一个私有字段,只有通过这里加锁和解锁,于是怀疑是其他线程获取到该锁以后执行阻塞了。

但是通过检查完整的线程栈,发现并没有其他线程阻塞在该方法获取到锁以后的情况。

查看内存对象

jmap -dump:format=b,file=8166.dump 8166

通过以上命令导出内存dump文件下载到本地,使用VisualVM加载dump文件。

找到QueueMiddleComponent对象,发现只有一个实例,其中writeLock字段的关键信息如下
1

可以看到这个锁当前被名为DubboServerHandler-192.168.160.42:9184-thread-93的线程所持有,于是再从线程栈里面找到这个线程的当前栈信息

"DubboServerHandler-192.168.160.42:9184-thread-93" #259 daemon prio=5 os_prio=0 tid=0x0000000004b92800 nid=0x22b5 waiting on condition [0x00007f2cedc33000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000008a518f80> (a java.util.concurrent.SynchronousQueue$TransferStack)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:458)
	at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
	at java.util.concurrent.SynchronousQueue.take(SynchronousQueue.java:924)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

可以看到这个线程已经执行完框架层的代码,正在等待新的请求,意味着这个锁已经不会再被释放了,那么是什么原因导致这个线程在执行完框架代码的时候没有释放writeLock这个锁的呢?

定位原因

相信眼尖的同学已经发现了上面那段加锁解锁代码中的问题了,解释如下

try {
    //如果wb.close()抛出异常,则不会执行unlock
    wb.close();
    writeLock.unlock();
} catch (IOException e) {
    //如果抛出的不是IOException,则不会执行unlock
    writeLock.unlock();
    throw new IllegalStateException(e.getMessage());
}

就是说在代码wb.close()执行的时候抛出了非IOException异常,然后没有被捕获住,所以导致释放锁代码writeLock.unlock();没有被执行就结束了。

解决问题

知道了原因,解决就简单了,只要稍微改造一下就可以了

try {
    wb.close();
} catch (IOException e) {
    throw new IllegalStateException(e.getMessage());
} finally {
    writeLock.unlock();
}

只要把writeLock.unlock();代码移动到finally块中,保证即时出现异常,也能正常解锁就行了。

总结

简单来说,这是一次使用锁不恰当而导致的连锁反应,,因为其中一个线程异常退出没有解锁,导致其他进来的线程一旦进入到这个方法就会被阻塞,dubbo的线程数是有限的(默认200),当所有线程都被阻塞的时候,dubbo就完全不能提供服务了。

吸取一下经验

  1. 解锁代码要放在finally块中,保证即使线程异常,也能正常解锁。
  2. 如果需要加锁执行的代码,最好能做成异步执行,这样即使阻塞也只是阻塞异步线程池,不会影响主工作线程的正常执行。

标签:Dubbo,java,使用不当,util,concurrent,writeLock,线程,ThreadPoolExecutor
From: https://www.cnblogs.com/martindai/p/18240743

相关文章

  • 线程池原理及c语言实现线程池
    线程池线程池是一种多线程处理机制,其主要目的是提高系统资源利用率、降低系统资源消耗,并通过控制并发线程数量来优化性能。以下是关于线程池的详细解释:定义:线程池是一种线程使用模式,它维护着一组线程,这些线程等待监督管理者分配可并发执行的任务。通过将任务添加到队列中,并......
  • python实现自定义线程池
    线程池ThreadPool对象的几个关键方法:get_idle_num():获得当前空闲线程的数量submit(task:callable):把一个任务(实际上就是一个函数)提交到线程池中执行.如果没有空闲线程则阻塞.wait_idle():阻塞,直到有空闲线程stop():停止线程池中的所有线程.(注意:非强制停止,......
  • 第1讲:进程和线程
    扫盲课。对Linux系统下,进程和线程的基本概念和对比进行阐述。一、进程进程是处于执行期的程序及相关资源的总称。操作系统为进程提供两种虚拟机制:虚拟处理器&虚拟内存,目的是让进程有一种假象:“独享处理器和整个内存空间”。关于进程描述符structtask_struct放在后续内容......
  • 线程池代码详解
    线程池概念线程池是一种基于池化技术的多线程管理机制。在这种模式下,一组线程被创建并且维护在一个"池"中,这些线程可以被循环利用来执行多个任务。当有新的任务到来时,线程池会尝试使用已经存在的空闲线程,而不是每次都创建新线程。这样做的目的是为了减少因频繁创建和销毁线程所带......
  • JUC及多线程,线程安全
    JUC及多线程返回到Java开发知识汇总目录@程序员猴哥1.什么是JUCjava.util.concurrent:核心并发工具类。java.util.concurrent包含了许多线程安全,测试良好,高性能的并发模块。创建java.util.concurrent的目的就是要实现Collection框架对数据结构所执行的并发操作。核心......
  • 【QT5】<总览五> QT多线程、TCP/UDP
    文章目录前言一、QThread多线程二、QT中的TCP编程1.TCP简介2.服务端程序编写3.客户端程序编写4.服务端与客户端测试三、QT中的UDP编程1.UDP简介2.UDP单播与广播程序前言承接【QT5】<总览四>QT常见绘图、图表及动画。若存在版权问题,请联系作者删除!一、QThre......
  • android主线程与子线程
    创建子线程创建子线程在android中穿件子线程的方案很简单创建子线程的几种方法///////第一种///////classThreadoneextendsThread{@Overridepublicvoidrun(){}//重写run方法,这个方法就是子线程一旦启用就会执行的方法}newThreadone().start()//启动子线程/......
  • python 字典是不是线程安全的
    Python字典(dict)对象本身不是线程安全的。在多线程环境下,对同一个字典对象的读写操作需要额外的同步机制来确保线程安全性。如果需要在多线程环境下使用线程安全的字典,可以使用collections.Counter对象,它是线程安全的,或者使用threading.local,它提供了线程局部存储的功能。另外......
  • 线程池的实现代码分析
    [toc]线程池线程池代码分析thread_pool.c#include"thread_pool.h"voidhandler(void*arg){ printf("[%u]isended.\n", (unsigned)pthread_self()); //打印自己的进程号 pthread_mutex_unlock((pthread_mutex_t*)arg); //解锁}//线程要执行的任......
  • Dubbo 3.x源码(21)—Dubbo服务引用源码(4)
    基于Dubbo3.1,详细介绍了Dubbo服务的发布与引用的源码。此前我们学习了createInvokerForRemote方法中的Wrapper有哪些以及作用,接下来我们将会的学习真正的本地、应用级别、接口级别的Protocol的引入逻辑,以及创建Proxy服务接口代理对象的逻辑。Dubbo3.x服务引用源码:Dub......