ReentrantLock浅析

ReentrantLock是什么?

ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似。不过,ReentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantLock里面有一个内部抽象类Sync,Sync继承了AQS(AbstractQueuedSynchronizer)类,添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
Sync有公平锁和非公平锁两个子类,分别是公平锁FairSync类和非公平锁NonfairSync类
ReentrantLock构造函数如下,如果不传参数那么就初始化那么ReentrantLock默认使用非公平锁。

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

公平锁和非公平锁的区别

公平锁:锁被释放之后,先申请的线程先得到锁,性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。但减少了线程饥饿的可能性。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

ReentrantLock做了什么?

ReentrantLock比synchronized增加了一些高级功能,主要来说主要三点:

(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁(等待可中断)

(2)ReentrantLock可以获取各种锁的信息

(3)ReentrantLock可以灵活地实现多路通知

两者都是可重入锁
synchronized是JVM级别的,而ReentrantLock是API级别;
synchronized内置在JVM里面,而ReentrantLock是由代码实现的

ReentrantLock是怎么上锁的

线程加锁的流程是:.lock() -> .acquire() -> tryAcquire()
ReentrantLock类继承了 lock 接口,所以需要实现 lock() 方法
而ReetrantLock的lock()函数的实现,是调用了内部类Sync的lock()抽象方法,该方法在FairSync和NonfairSync中有不同的实现
FairSync中的lock直接调用了 acquire 方法
而NonfairSync会先尝试进行锁的获取,先调用 compareAndSetState() 争取一次,失败的话再调用 acquire 方法

并且两者的 tryAcquire 方法也不同
NonfairSync 类中执行 tryAcquire, 当 getState == 0 时会直接去获取锁
而FairSync类中 tryAcquire ,即是 getState == 0 ,也需要先判断是否有线程在队列中排队,如果有的话就也要返回false

tryAcquire方法获取锁失败的话就会调用acquireQueued把当前线程放到等待队列里面去

AQS中的acquire方法运行流程

如果tryAcquire返回false,没有获取锁,那么就会使用addWait将当前节点添加到等待队列,然后调用acquireQueued

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

在acquireQueued中,如果 p == head && tryAcquire(arg) 成功就 return interrupted(false),也就不会在acquire中自我中断
再次尝试加锁,非公平锁在这里就会再次抢一次锁
如果失败那么会通过 shouldParkAfterFailedAcquire 自旋一次,下一次依旧获取锁失败,那么就调用shouldParkAfterFailedAcquire判断是否应该调用park方法进入睡眠。

shouldParkAfterFailedAcquire(应该在获取锁失败后沉睡吗)
为什么在park前需要调用shouldParkAfterFailedAcquire判断是否应该调用park方法进入睡眠呢?
因为当前节点的线程进入park后只能被前一个节点唤醒,那前一个节点怎么知道有没有后继节点需要唤醒呢?因此当前节点在park前需要给前一个节点设置一个标识,即将waitStatus设置为Node.SIGNAL(-1),然后自旋一次再走一遍刚刚的流程,若还是没有获取到锁,则调用parkAndCheckInterrupt进入睡眠状态。

parkAndCheckInterrupt(睡眠且检查是否中断过)
parkAndCheckInterrupt通过 LockSupport.park(this)进入睡眠后,有两种情况会被唤醒
一种是前一个节点持有锁,且锁被释放,那么前一个节点在执行 un.lock的时候会执行 LockSupport.unpark(s.thread) ,这里的被唤醒的s.thread就是当前线程,这种情况唤醒后parkAndCheckInterrupt 会返回false,表示没有被中断过,那么acquireQueued中的interrupted也就不会被设置为True
还有一种那就是被中断了,那么此种情况 parkAndCheckInterrupt会 返回 True,那么interrupted被设置为interrupted,

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//直到p节点是node节点的上一个节点
                if (p == head && tryAcquire(arg)) {//如果p节点是head,那么再次使用tryAcquire尝试获取锁。如果获取成功就将头结点设置为当前节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
            
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

那么Thread.interrupted这个方法是做什么用的呢

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

这个是用来判断当前线程是否被打断过,并清除打断标记(若是被打断过则会返回true,并将打断标记设置为false),所以调用lock方法时,通过interrupt也是会打断睡眠的线程的,只是Doug Lea做了一个假象,让用户无感知;但有些场景又需要知道该线程是否被打断过,所以acquireQueued最终会返回interrupted打断标记,如果是被打断过,则返回的true,并在acquire方法中调用selfInterrupt再次打断当前线程(将打断标记设置为true)。

ReetrantLock可重入锁的具体实现步骤

NonfairSync的tryAcquire() 实现直接调用了Sync抽象类中已经实现的nonfairTryAcquire()方法:

    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;
    }

首先获取当前线程
getState() 方法是AQS类里面定义的,返回当前状态值,0表示未上锁状态,非0表示上锁状态.注意:这里的State可以设置为1,2,3,4…表示有多少重锁
如果c = 0,表示当前 lock对象 未上锁,那么调用compareAndSetState()方法,和CAS类似,比较然后设置State值
该方法AQS类中的方法,最后的实现是调用的 Unsafe 类中的一个native方法,表示由外部实现,非JAVA代码实现(Unsafe 类经常用于执行一些原子操作)
compareAndSetState 如果成功上锁,那么会返回True。将ReentrantLock在 AbstractOwnableSynchronizer 类中的 exclusiveOwnerThread 变量设置为当前的线程。
先把State设置为1,才能 setExclusiveOwnerThread
exclusiveOwnerThread 作用是记录当前持有锁的线程
如果c != 0,表示当前 lock对象 已经被上锁,那么这里就是 ReentrantLock实现可重入锁的核心代码
判断当前线程是否在AbstractOwnableSynchronizer 类中记录的 exclusiveOwnerThread,也即是持有锁的线程
如果当前线程就是持有锁的线程,那么将 State 的值设置为 State + 1 (acquire的值一般都是1)
如果c != 0,且当前线程不是锁的持有者,那么返回false,代表获取锁失败

FairSync的tryAcquire()方法:

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            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;
    }

与NonFairSync中不同的就是当 c == 0时,也不能直接进行上锁操作,需要先执行 hasQueuedPredecessors() 方法判断是否存在前驱节点(线程):
如果 hasQueuedPredecessors() 返回 true,这意味着当前线程前面有其他线程在等待,因此当前线程不能立即尝试获取锁,它应该等待直到前面的线程释放锁。
如果 hasQueuedPredecessors() 返回 false,这意味着当前线程是队列中的第一个等待线程,它可以尝试通过 compareAndSetState(0, acquires) 来获取锁。
如果 !hasQueuedPredecessors() 返回false,那么就会把当前的线程放到队列当中去

可中断锁和不可中断锁有什么区别

可中断锁:获取锁的过程可以被中断,不需要一直等到获取锁之后,才能进行其他逻辑处理。ReentrantLock就属于是可中断锁
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。synchronized就属于不可中断锁

相关的技术
ReadWriteLock

首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现
实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值