什么,这就是可重入锁?

1,145 阅读6分钟

前言

前些天去面了XXX公司,很菜的我连初面都没过。虽然已经六七年的开发经验了,虽然也带着项目组装模作样的打怪升级。但还是逃不过一些基础问题的连环call。知耻而后勇,打算好好恶补一下。可重入锁是我没有回答好的问题之一,今天就来好好学习一下什么TM叫可重入锁。

问答环节

面试官:请你谈谈可重入锁?
我:嗯~,我用过ReentrantLock。
面试官:具体讲讲呢。
我:。。。
愉快的面试结束了。

什么是可重入锁?

其实大部程序员应该都知道什么是可重入锁,在同一个线程中可以多次获取同一把锁。那只能回答个概念显然是满足不了如今八股文面试的。所以今天就通过文章的形式来让自己比较全面的、具体的认识一下什么是可重入锁。
首先要问自己一个问题,为什么要可重入,可重入的意义是什么?这也就首先要抛出场景。

场景

假如有两个方法,一个方法叫synchronized goHome(),一个方法叫synchronized goToSleep()。可以看到这两个方法都是synchronized修饰的。看下具体代码:

public class SynchronizedDemo {

    public synchronized void goHome() {
        System.out.println("我已经到家啦,洗洗准备睡了!");
        this.goToSleep();
    }

    public synchronized void goToSleep() {
        System.out.println("我睡觉啦,你别过来啊~");
    }

    public static void main(String[] args) {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        synchronizedDemo.goHome();
    }

}

执行结果:

image.png
从执行结果可以看出 goToSleep() 方法正常调用了,并没有被阻塞,这也是我们想要的结果。由此可以看出synchronized 也是可重入锁,我在面试中的回答,显然告诉了面试官我有多菜。
以上例子我大概可以明白可重入锁是个啥了。下面再回答一下面试官的问题,具体谈谈什么是可重入锁。

Java中的可重入锁

Java中的synchronizedReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。本文重点讨论ReentrantLocksynchronized将单独写一遍再做学习。

先看下ReentrantLock的继承关系图:

image.png

ReentrantLock实现了Lock接口,对外提供Lock接口的方法。有一个同步器属性,上锁、释放锁都是通过调用同步器的相关方法实现的。构造时,同步器可以选择公平锁/非公平锁,它们都继承了抽象父类Sync,而Sync又继承了AQS
抽象类AQS维护了等待队列,而ReentrantLock只需要定义共享资源的获取与释放的方式。

可重入功能的实现原理

ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。

// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state的值
    int c = getState();
    // 如果state的值等于0,表示当前没有线程持有锁
    // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state的值不等于0,表示已经有其他线程持有锁
    // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
    // 如果不是当前线程,获取锁失败,返回false
    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;
}

非公平锁的实现原理

因为ReentrantLock同时支持公平锁和非公平锁,所以这里再讲一下非公平锁的实现原理。 ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁。
对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会入队列。
那么对于非公平锁,显然就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,所以是非公平的。

非公平得获取锁:

// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state的值
    int c = getState();
    // 如果state的值等于0,表示当前没有线程持有锁
    // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state的值不等于0,表示已经有其他线程持有锁
    // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
    // 如果不是当前线程,获取锁失败,返回false
    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;
}

公平得获取锁:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // 获取锁,与非公平锁的不同的地方在于,这里直接调用的AQS的acquire方法,没有先尝试获取锁
    // acquire又调用了下面的tryAcquire方法,核心在于这个方法
    final void lock() {
        acquire(1);
    }

    /**
     * 这个方法和nonfairTryAcquire方法只有一点不同,在标注为#1的地方
     * 多了一个判断hasQueuedPredecessors,这个方法是判断当前AQS的同步队列中是否还有等待的线程
     * 如果有,返回true,否则返回false。
     * 由此可知,当队列中没有等待的线程时,当前线程才能尝试通过CAS的方式获取锁。
     * 否则就让这个线程去队列后面排队。
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // #1
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

总结

由于synchronized是基于monitor机制实现的,它只支持非公平锁;但ReentrantLock同时支持公平锁和非公平锁。ReentrantLock同时支持公平锁与非公平锁,支持:尝试非阻塞的一次性获取锁 ,支持超时获取锁,支持可中断的获取锁,支持更多的等待条件(Condition)。 结合了一些文章和自己的理解,整理成为加深自己对知识的理解,可中断获取锁、支持等待条件相关知识没有记录,有不准确的地方欢迎大家批评指正。