java程序的锁泄漏的解决方案

145 阅读4分钟

背景

java引入显示锁提升性能,显示锁虽然是提升了性能,但是带来编程的繁琐,必须明确的调用lock和unlock,代码块在同一个方法的时候还能强行用finally来保障锁一定在执行的时候释放。在复杂的场景中,申请锁和释放锁不在同一个代码块中,这就导致了很多异常的情况,会导致锁没有成功释放,新的锁的申请会一直等待。

导致这个现象可能有如下原因

  1. 代码处理异常问题,导致最终没有能释放锁。
  2. 线程意外退出了,例如被外层杀掉了。

上面2个原因的解决方式相差很大,找对原因对解决问题帮助很大。

日志解法

日志可以说是适用性最广的解法了,前提是编码的时候需要明确写下进行到哪一步了,如果有外部控制线程的地方也明确的打出停止线程的地方。通过互相对账。就可以找出问题了。这个方法在实施上有一定的难度。

困难如下:

  1. 每个编码人员编写操作时,是否能按要求编写日志。
  2. 代码是多人协同编写的,做一些代码变更的时候,日志可能失效或者误删。

所以我们可以尝试从底层来看做一些分析。

锁的特征

下面以ReentrantLock为例来刻画一下怎么去找出一把锁。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

这是非公平锁获取锁的代码,他查看锁的状态位,如果是0就表示还没有线程获取这个锁,然后就会通过cas更改锁的状态位,然后执行setExclusiveOwnerThread,这里把线程信息留下了。下面是申请锁是当前线程,就直接把状态位+1。这里就是做了重入,同一个线程可以多次获取锁。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

锁的释放也是同样的操作,更改状态位,然后把setExclusiveOwnerThread把线程的信息清空。

重点来了,这里的线程信息是很重要的。

private transient Thread exclusiveOwnerThread;

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

成员变量就是一个thread对象。

通过以上的信息,我们可以得到这样一个结论: 获取到ReentrantLock锁的线程,已经会被设置在exclusiveOwnerThread中。基于这个特点。我们就有了新的解法。这就是一个内存问题,在内存对象中找出锁对象,然后查看他的成员变量就好。

heapdump解法

想要查看内存,最简单的方法就是打heapdump。执行如下命令即可。

jmap -dump:live,format=b,file=heap.bin <pid>

这里我们使用mat来解析heapdump,这里大家一定有一个疑问,heapdump里的对象那么多,怎么能找出信息呢。这里就需要用到mat的oql功能。mat提供了类似sql的表达,他后台会转化成解析代码去检索对象。因为编写oql特别麻烦,所以这里直接贴出结果方便大家使用。

select * from java.util.concurrent.locks.ReentrantLock$NonfairSync s where s.exclusiveOwnerThread != null

这里做一下解读。
ReentrantLock$NonfairSync是指得非公平锁,如果要选择公平锁,这里得替换一下新的内部类。 后面的条件很好理解就是exclusiveOwnerThread字段不为null 这里就可以筛选出当时锁被占用的锁对象了,然后层层点开就可以看到thread信息,这个信息就帮助我们找到线程。

heapdump会有线程堆栈的信息,这里从mat上检索即可。

下面根据情况来说明的一下场景。

  1. 线程还没走到释放锁的代码。这里就是卡主了,解决卡的问题。
  2. 线程走完了锁的代码,通过heap可以看到当前的堆栈,用来判断是否走完了。
  3. 线程已经找不到了,有2种情况,第一种是线程正常退出,第二种是有管理能力执行了stop。

那这里还需要区分stop的情况,建议把jfr开启,遇到问题dump一下发生的记录。就可以知道是否执行过stop函数了。

小结

弥补常规解法的问题,我们加入了heapdump解法,结合jfr,就可以在不需要充足的日志的情况下,找到了不能正确释放锁的地方。