基于ReentrantLock实现的AQS原理

AQS是什么?

AQS:AbstractQueuedSynchronizer,抽象队列同步器,它主要由两个部分组成,一个由volatile修饰的state变量(代表共享的资源)和一个双向队列(用头节点和尾结点来表示)。
AQS类中定义了队列中结点node的类,以及定义了获取锁的方法(tryRequire())和释放锁的方法(tryRealease());
然后定义了一个抽象模板类Sync类继承AQS类,写了释放锁的方法实现,然后公平锁类(FairSync)和非公平锁类(NonFairSync)分别实现不同的获取锁方法。

ReentrantLock是什么?

ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下。能保证共享数据安全性,线程间有序性。其底层正是通过一个AQS抽象队列同步器实现独占锁模式达到目的的。

调用ReentrantLock.lock()方法会发生什么?

ReentrantLock默认是非公平锁,也有公平锁的实现。这里,我们先来讲,当实现是非公平锁时,调用它的lock方法会发生什么? 首先回去判断state变量是否为0,当为0时,则说明该资源没有被别的线程占用,则通过CAS操作把state改为1,如果CAS操作成功,则设置锁占用的线程为当前线程名字;如果CAS操作不成功,则进行入队操作。如果state变量不为0,则说明锁被别的线程占用,查看锁占用的线程是不是当前线程名字,如果是,因为ReentrantLock是支持可重入的,所以进行state+1。
第一次入队操作时由于AQS还没有进行初始化,需要先进行一个头节点和尾节点初始化的操作,再把新入队的结点挂在尾节点的前面。然后去执行一个自旋操作(acquireQueued()方法),该方法首先会去判断如果你的前一个节点是头节点,则会去再进行一次CAS操作尝试获取锁,如果失败,则调用LockSupport.park()方法将当前线程阻塞起来,等待唤醒。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

调用ReentrantLock.unlock()方法会发生什么?

当占用锁的线程释放锁的时候,会去唤醒头节点后的第一个活跃的节点(由于ReentrantLock存在中断机制,即某线程一开始是想要这个锁的,但获取不到就加入了队列中,但一段时间后就不想要继续等下去了。这是通过节点node类中的Waitstatue变量实现的)。当唤醒了线程后,由于它仍然在acquireQueued方法中的循环了,所以会去再尝试用CAS操作获取锁。(这里就涉及到公平锁与非公平锁的区别了,当实现是公平锁的时候,新的线程调用lock方法时会去队列中查看是否还有节点,如果存在节点,则直接入队,如果没有节点,才尝试用CAS操作获取锁。而非公平锁的话是直接尝试用CAS操作获取锁),如果CAS操作成功,则把头节点设置为null,方便JVM的GC回收,把自己设置为头节点。

补充

park和unpark机制

每个线程都会关联一个 Parker 对象,每个 Parker 对象都各自维护了三个角色:计数器、互斥量、条件变量。

park 操作的步骤:
获取当前线程关联的 Parker 对象。
将计数器置为 0,同时检查计数器的原值是否为 1,如果是则放弃后续操作。
在互斥量上加锁。
在条件变量上阻塞,同时释放锁并等待被其他线程唤醒,当被唤醒后,将重新获取锁。
当线程恢复至运行状态后,将计数器的值再次置为 0。
释放锁。

unpark 操作:
获取目标线程关联的 Parker 对象(注意目标线程不是当前线程)。
在互斥量上加锁。
将计数器置为 1。
唤醒在条件变量上等待着的线程。
释放锁。
所以,如果当unpark先于park方法执行时,到执行park方法的时候,会先去检查计数器的原值是否为1,如果为1,说明unpark方法先执行了,则取消这一次park的操作,不会造成死锁。

Synchronized与ReentrantLock的区别

①其实 ReentrantLock 和 Synchronized 最核心的区别就在于 Synchronized 适合于并发竞争低的情况,因为 Synchronized 的锁升级如果最终升级为重量级锁在使用的过程中是没有办法消除的,意味着每次都要和 cpu 去请求锁资源,而 ReentrantLock 主要是提供了阻塞的能力,通过在高并发下线程的挂起,来减少竞争,提高并发能力。
②Synchronized 是一个关键字,是由 JVM 层面去实现的,而 ReentrantLock 是由 java api 去实现的。
③Synchronized 是隐式锁,可以自动释放锁;ReentrantLock 是显式锁,需要调用unlock方法手动释放锁,不然容易造成死锁。
④ReentrantLock 可以让等待锁的线程响应中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。而 Synchronized 却不行,使用 Synchronized 时,等待的线程会一直等待下去,不能够响应中断。
⑤ ReentrantLock可以通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
⑥锁的对象不一样。Synchronized 修饰需要同步的方法和需要同步代码块,默认是当前对象作为锁的对象,可以为类或者是类的实例。ReentrantLock是给当前线程加锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值