文章目录

  • 1. ReentrantLock概述
  • 2. 非公平锁
  • 2.1 加锁过程
  • 2.2 解锁过程
  • 3. 公平锁
  • 3.1 加锁过程
  • 3.2 解锁过程
  • 4. 总结
  • 5. 自定义锁


1. ReentrantLock概述

ReentrantLock意思为可重入锁,也就是能够多重加锁。并且加了多少次锁,也必须对应解锁多少次。

此外,ReentrantLock支持公平锁和非公平锁,是基于AQS进行实现的。关于公平锁和非公平锁可以看:【基本功】不可不说的Java“锁”事

  • 公平锁:按照AQS的FIFO的同步队列的时间顺序获取锁,不会存在饥饿现象。
  • 非公平锁:有可能刚刚释放锁的线程又重新获得锁,可能会出现线程饥饿现象。

下面我们来看下ReetrantLock和synchronizer之间的对比,如下图:

redisson 非公平锁 java非公平锁实现_公平锁

AQS中实现了独占锁和共享锁的框架。而ReentrantLock也是基于AQS进行实现的。只支持独占锁。

具体来说, ReentrantLock定义了静态内部内Sync,Sync类继承自AbstractQueuedSynchronizer。又因为ReentrantLock包括公平锁和非公平锁两种实现,所以又分别定义了FairSync和NonfairSync类,继承自Sync类,代表公平锁和非公平锁的各自实现。

从ReentrantLock的构造函数来看,它在默认情况下是创建非公平锁的

public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

因为非公平锁相比于公平锁,可能在某些情况下可以减少上下文的切换,节省资源的消耗。

2. 非公平锁

2.1 加锁过程

我们首先进入非公平锁的lock()函数中:

final void lock() {
        if (compareAndSetState(0, 1))  // CAS尝试获取锁
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);  // 进入AQS的代码中尝试获取锁
    }

从上面的代码中可以看到,一上来就尝试用CAS去修改state,也就是尝试获取锁(并没有排队),因为state就是AQS用来同步状态的。如果获取成功,那就插队成功了.

如果获取失败,就会进入AQS中的acquire函数继续尝试获取:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  // 尝试获取锁
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

由我们之前讲过的AQS原理可知,AQS中的tryAcquire方法是空方法,需要自定义的同步器进行实现。所以我们这里就会调用ReentrantLock中NonfairSync重写的tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);  // 进一步调用
    }

进一步调用nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread(); // 获取当前线程
        int c = getState();  // 获取当前的state
        if (c == 0) {  // 如果没有线程获取锁
            if (compareAndSetState(0, acquires)) {  // 尝试CAS获取锁
                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);   // 更新state
            return true;
        }
        return false;  // 获取锁失败
    }

从上面的方法中可以看到,如果state=0,那么就立马去尝试抢锁。如果发现本身持有锁的就是当前线程,那么就可以通过增加state进行重入。

如果通过tryAcquire方法抢锁失败,那么就只能老老实实地回到AQS的队列中进行排队了。

整个的流程图如下:

redisson 非公平锁 java非公平锁实现_redisson 非公平锁_02

2.2 解锁过程

解锁过程我们首先会进入到unlock函数中:

public void unlock() {
        sync.release(1);
    }

接着我们会进入AQS的release函数:

public final boolean release(int arg) {
        if (tryRelease(arg)) {  // 尝试解锁
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);  // 唤醒后面的进程
            return true;
        }
        return false;
    }

然后就会调用tryRelease函数尝试解锁,但是在AQS中tryRelease函数是空函数,需要自定义的同步器自己实现。所以我们就会进入到ReentrantLock的Sync类中的tryRelease函数中(公平锁和非公平锁的解锁过程是一样的)。

protected final boolean tryRelease(int releases) {
        int c = getState() - releases;   // 在state上减少重数
        if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果发现锁不是当前线程的,则抛出异常
            throw new IllegalMonitorStateException();
        boolean free = false;  // 判断是否完全释放的标志位
        if (c == 0) {  // 如果加锁重数=解锁重数,那么就完全释放了
            free = true;   // 完全解锁
            setExclusiveOwnerThread(null);
        }
        setState(c);  // 更新state
        return free;  // 返回是否完全释放锁
    }

解锁成功后,就继续回到AQS唤醒后续的进程。解锁结束。

3. 公平锁

3.1 加锁过程

首先是会进入到公平锁的lock函数中:

final void lock() {
        acquire(1);   // 尝试获取锁
    }

可以看到,他这里是直接调用了AQS中的acquire函数:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

根据上面的非公平锁的介绍,可以知道tryAcquire会调用自定义的同步器中重写的方法。

因此我们进入到ReentrantLock中FairSync类的tryAcquire函数:

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();  // 获得当前线程
        int c = getState();  // 获取state
        if (c == 0) {   // 如果当前没有人持有锁
            if (!hasQueuedPredecessors() &&   // 队列是否有前驱
                compareAndSetState(0, acquires)) {  // CAS尝试持有锁
                setExclusiveOwnerThread(current);  // 设置当前线程独占
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {  // 如果本身是当前线程持有错
            int nextc = c + acquires;  // 进行重入
            if (nextc < 0)  // 重数过多
                throw new Error("Maximum lock count exceeded");
            setState(nextc);  // 更新state
            return true;  
        }
        return false;  // 获取锁失败
    }
}

可以看到,公平锁和非公平锁的tryAcquire,主要区别就是在公平锁中,只有队伍中没有前驱节点,才会去尝试CAS获得锁。而在非公平锁中,不管当前等待队伍中是否有节点,都会直接尝试用CAS获得锁。

如果在tryAcquire中没有获取到锁,就会重新进入到AQS的队列中进行排队了。

3.2 解锁过程

在ReentrantLock的公平锁和非公平锁中,解锁过程都是一样的,都是在ReentrantLock中,所以这里就不过多赘述。

4. 总结

ReentrantLock其实是基于AQS框架来实现的一个同步器。以非公平锁为例,我们通过一张图来看下ReentrantLock整个加锁和解锁的过程:

redisson 非公平锁 java非公平锁实现_公平锁_03

可以看到,ReentrantLock构建了对外的两个api lock和unlock,并且重写了AQS中需要实现的方法tryAcquire、tryRelease。而AQS就主要是提供一个框架的作用,完成了锁的底层实现,使得ReentrantLock不需要关心在底层具体要如何实现。具体如下图:

redisson 非公平锁 java非公平锁实现_ReentrantLock实现原理_04

5. 自定义锁

首先,我们自定义一个类Mutex,用来对外提供锁的api接口。然后,我们在Mutex中创建静态内部类Sync(AQS推荐同步器使用静态内部类继承的方式实现),重写tryAcquire和tryRelease方法。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class Mutex {
    private Sync sync = new Sync();

    public void lock(){   // 对外的加锁api
        sync.acquire(1);
    }

    public void unlock(){  // 对外的解锁api
        sync.release(1);
    }

    // 自定义的同步器
    private static class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {  // 重写AQS中的tryAcquire方法
            return compareAndSetState(0,arg);
        }

        @Override
        protected boolean tryRelease(int arg) {  // 重写AQS中的tryRelease方法
            setState(0);
            return true;
        }
    }
}

至此,我们就很轻松地自定义了一个锁。

接下来,我们写一个类进行测试。多线程对静态变量count进行+1操作。

public class TestCount {
    public static Mutex mutex = new Mutex();  // 自定义的锁
    public static int count = 0;  // 共享变量count

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            public void run() {
                try {
                    mutex.lock();
                    for(int i=0;i<10000;i++){
                        count++;
                    }
                }finally {
                    mutex.unlock();
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(TestCount.count);
    }
}

因为我们在自增的前后加入了锁进行同步,这就使得每次输出的结果都是20000。

参考文章:从ReentrantLock的实现看AQS的原理及应用