重入锁(ReentrantLock)

一  概述

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

  • 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

公平与非公平获取锁的区别

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同,唯一不同的位置为判断条件多了
hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

二  源码分析

回忆在同步器一节中的示例(Mutex),同时考虑如下场景:当一个线程调用Mutex的lock()方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己所阻塞,原因是Mutex在实现tryAcquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用tryAcquire(int acquires)方法时返回了false,导致该线程被阻塞。简单地说,Mutex是一个不支持重进入的锁。

ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。

接下来我们从源码角度来看看ReentrantLock的实现原理,它是如何保证可重入性,又是如何实现公平锁的??

  ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。

无参构造器(默认为非公平锁)

public ReentrantLock() {
        sync = new NonfairSync();//默认是非公平的
    }

sync是ReentrantLock内部实现的一个同步组件,它是Reentrantlock的一个静态内部类,继承于AQS,后面我们再分析。

带布尔值的构造器(是否公平)

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁
    }

看到了吧,此处可以指定是否采用公平锁,FailSync和NonFailSync亦为Reentrantlock的静态内部类,都继承于Sync

再来看看几个我们常用到的方法

lock()

public void lock() {
        sync.lock();//代理到Sync的lock方法上
    }

Sync的lock方法是抽象的,实际的lock会代理到FairSync或是NonFairSync上(根据用户的选择来决定,公平锁还是非公平锁)

lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//代理到sync的相应方法上,同lock方法的区别是此方法响应中断
    }

此方法响应中断,当线程在阻塞中的时候,若被中断,会抛出InterruptedException异常 

tryLock()

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);//代理到sync的相应方法上
    }

tryLock,尝试获取锁,成功则直接返回true,不成功也不耽搁时间,立即返回false。

unlock()

public void unlock() {
        sync.release(1);//释放锁
    }

释放锁,调用sync的release方法,其实是AQS的release逻辑。

 newCondition()    获取一个conditon,ReentrantLock支持多个Condition

public Condition newCondition() {
        return sync.newCondition();
    }

其他方法就不再赘述了,若想继续了解可去API中查看。

小结

  其实从上面这写方法的介绍,我们都能大概梳理出ReentrantLock的处理逻辑,其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。

  接下来,关于如何实现重入性,如何实现公平性,就得去看这几个静态内部类了

NonFairSync(非公平可重入锁)

static final class NonfairSync extends Sync {//继承Sync
        private static final long serialVersionUID = 7316153563782823691L;
        /** 获取锁 */
        final void lock() {
            if (compareAndSetState(0, 1))//CAS设置state状态,若原值是0,将其置为1
                setExclusiveOwnerThread(Thread.currentThread());//将当前线程标记为已持有锁
            else
                acquire(1);//若设置失败,调用AQS的acquire方法,acquire又会调用我们下面重写的tryAcquire方法。这里说的调用失败有两种情况:1当前没有线程获取到资源,state为0,但是将state由0设置为1的时候,其他线程抢占资源,将state修改了,导致了CAS失败;2 state原本就不为0,也就是已经有线程获取到资源了,有可能是别的线程获取到资源,也有可能是当前线程获取的,这时线程又重复去获取,所以去tryAcquire中的nonfairTryAcquire我们应该就能看到可重入的实现逻辑了。
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//调用Sync中的方法
        }
    }

nonfairTryAcquire() 

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取当前state值
            if (c == 0) {//若state为0,意味着没有线程获取到资源,CAS将state设置为1,并将当前线程标记我获取到排他锁的线程,返回true
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//若state不为0,但是持有锁的线程是当前线程
                int nextc = c + acquires;//state累加1
                if (nextc < 0) // int类型溢出了
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//设置state,此时state大于1,代表着一个线程多次获锁,state的值即是线程重入的次数
                return true;//返回true,获取锁成功
            }
            return false;//获取锁失败了
        }

简单总结下流程:

    1.先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;

    2.若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;

    3.其他情况,则获取锁失败。

   来看看可重入公平锁的处理逻辑

FairSync

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//直接调用AQS的模板方法acquire,acquire会调用下面我们重写的这个tryAcquire
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取state值
            if (c == 0) {//若state为0,意味着当前没有线程获取到资源,那就可以直接获取资源了吗?NO!这不就跟之前的非公平锁的逻辑一样了嘛。看下面的逻辑
                if (!hasQueuedPredecessors() &&//判断在时间顺序上,是否有申请锁排在自己之前的线程,若没有,才能去获取,CAS设置state,并标记当前线程为持有排他锁的线程;反之,不能获取!这即是公平的处理方式。
                    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;
        }
    }

可以看到,公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。

  看看这个判断是否有排队中线程的逻辑

hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
        Node t = tail; // 尾结点
        Node h = head;//头结点
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());//判断是否有排在自己之前的线程
    }

 需要注意的是,这个判断是否有排在自己之前的线程的逻辑稍微有些绕,我们来梳理下,由代码得知,有两种情况会返回true,我们将此逻辑分解一下(注意:返回true意味着有其他线程申请锁比自己早,需要放弃抢占)

  1. h !=t && (s = h.next) == null,这个逻辑成立的一种可能是head指向头结点,tail此时还为null。考虑这种情况:当其他某个线程去获取锁失败,需构造一个结点加入同步队列中(假设此时同步队列为空),在添加的时候,需要先创建一个无意义傀儡头结点(在AQS的enq方法中,这是个自旋CAS操作),有可能在将head指向此傀儡结点完毕之后,还未将tail指向此结点。很明显,此线程时间上优于当前线程,所以,返回true,表示有等待中的线程且比自己来的还早。

  2.h != t && (s = h.next) != null && s.thread != Thread.currentThread()。同步队列中已经有若干排队线程且当前线程不是队列的老二结点,此种情况会返回true。假如没有s.thread !=Thread.currentThread()这个判断的话,会怎么样呢?若当前线程已经在同步队列中是老二结点(头结点此时是个无意义的傀儡结点),此时持有锁的线程释放了资源,唤醒老二结点线程,老二结点线程重新tryAcquire(此逻辑在AQS中的acquireQueued方法中),又会调用到hasQueuedPredecessors,不加s.thread !=Thread.currentThread()这个判断的话,返回值就为true,导致tryAcquire失败。

  最后,来看看ReentrantLock的tryRelease,定义在Sync中

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//减去1个资源
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //若state值为0,表示当前线程已完全释放干净,返回true,上层的AQS会意识到资源已空出。若不为0,则表示线程还占有资源,只不过将此次重入的资源的释放了而已,返回false。
            if (c == 0) {
                free = true;//
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

三  总结 

ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,才允许去抢占。

事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

参考博客 :https://www.cnblogs.com/chengxiao/p/7255941.html

《Java并发编程的艺术》第五章 (Java中的锁)

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值