面试:为了进阿里,须要深刻理解ReentrantLock原理

该系列文章收录在公众号【Ccww技术博客】,原创技术文章早于博客推出

前言

在面试,不少时间面试官都会问到锁的问题,ReentrantLock也是常问一个点,但具体会问什么呢?在网上收集到一些问题:java

  • 重入锁是什么?
  • 公平锁和非公平锁是什么?有什么区别?
  • ReentrantLock::lock公平锁模式现实node

    • ReentrantLock如何实现公平锁?
    • ReentrantLock如何实现可重入?
  • ReentrantLock公平锁模式与非公平锁获取锁的区别?
  • ReentrantLock::unlock()释放锁,如何唤醒等待队列中的线程?
  • ReentrantLock除了可重入还有哪些特性?
  • ReentrantLock与Synchrionized的区别
  • ReentrantLock使用场景

那么重入锁是什么?有什么用呢面试

ReentrantLock是什么?

ReentrantLock是个典型的独占模式AQS,同步状态为0时表示空闲。当有线程获取到空闲的同步状态时,它会将同步状态加1,将同步状态改成非空闲,因而其余线程挂起等待。在修改同步状态的同时,并记录下本身的线程,做为后续重入的依据,即一个线程持有某个对象的锁时,再次去获取这个对象的锁是能够成功的。若是是不可重入的锁的话,就会形成死锁。安全

ReentrantLock会涉及到公平锁和非公平锁,实现关键在于成员变量sync的实现不一样,这是锁实现互斥同步的核心。函数

//公平锁和非公平锁的变量
 private final Sync sync;
 //父类
 abstract static class Sync extends AbstractQueuedSynchronizer {}
 //公平锁子类
 static final class FairSync extends Sync {}
 //非公平锁子类
 static final class NonfairSync extends Sync {}

那公平锁和非公平锁是什么?有什么区别?性能

那公平锁和非公平锁是什么?有什么区别?

公平锁是指当锁可用时,在锁上等待时间最长的线程将得到锁的使用权,即先进先出。而非公平锁则随机分配这种使用权,是一种抢占机制,是随机得到锁,并非先来的必定能先获得锁。ui

ReentrantLock提供了一个构造方法,能够实现公平锁或非公平锁:spa

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

虽然公平锁在公平性得以保障,但由于公平的获取锁没有考虑到操做系统对线程的调度因素以及其余因素,会影响性能。操作系统

虽然非公平模式效率比较高,可是非公平模式在申请获取锁的线程足够多,那么可能会形成某些线程长时间得不到锁,这就是非公平锁的“饥饿”问题。线程

但大部分状况下咱们使用非公平锁,由于其性能比公平锁好不少。可是公平锁可以避免线程饥饿,某些状况下也颇有用。

接下来看看ReentrantLock公平锁的实现:

ReentrantLock::lock公平锁模式实现

首先须要在构建函数中传入true建立好公平锁

ReentrantLock reentrantLock = new ReentrantLock(true);

调用lock()进行上锁,直接acquire(1)上锁

public void lock() {
    // 调用的sync的子类FairSync的lock()方法:ReentrantLock.FairSync.lock()
    sync.lock();
}
final void lock() {
    // 调用AQS的acquire()方法获取锁,传的值为1
    acquire(1);
}

直接尝试获取锁,

// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
    // 尝试获取锁
    // 若是失败了,就排队
    if (!tryAcquire(arg) &&
        // 注意addWaiter()这里传入的节点模式为独占模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

具体获取锁流程

  • getState()获取同步状态state值,进行判断是否为0

    • 若是状态变量的值为0,说明暂时尚未人占有锁, 使用hasQueuedPredecessors()保证了不管是新的线程仍是已经排队的线程都顺序使用锁,若是没有其它线程在排队,那么当前线程尝试更新state的值为1,并本身设置到exclusiveOwnerThread变量中,供后续本身可重入获取锁做准备
    • 若是exclusiveOwnerThread中为当前线程说明自己就占有着锁,如今又尝试获取锁,须要将状态变量的值state+1

img

// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 状态变量的值为0,说明暂时尚未线程占有锁
    if (c == 0) {
        // hasQueuedPredecessors()保证了不管是新的线程仍是已经排队的线程都顺序使用锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 当前线程获取了锁,并将本线程设置到exclusiveOwnerThread变量中,
            //供后续本身可重入获取锁做准备
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    
    // 之因此说是重入锁,就是由于在获取锁失败的状况下,还会再次判断是否当前线程已经持有锁了
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置到state中
        // 由于当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
        // 因此不存在竞争,天然不须要使用CAS来更新
        setState(nextc);
        return true;
    }
    return false;
}

若是获取失败加入队列里,那具体怎么处理呢?经过自旋的方式,队列中线程不断进行尝试获取锁操做,中间是能够经过中断的方式打断,

  • 若是当前节点的前一个节点为head节点,则说明轮到本身获取锁了,调用tryAcquire()方法再次尝试获取锁

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 当前节点的前一个节点,
                final Node p = node.predecessor();
                // 若是当前节点的前一个节点为head节点,则说明轮到本身获取锁了
                // 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁
                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);
        }
    }
  • 当前的Node的上一个节点不是Head,是须要判断是否须要阻塞,以及寻找安全点挂起。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 上一个节点的等待状态
        int ws = pred.waitStatus;
        // 等待状态为SIGNAL(等待唤醒),直接返回true
        if (ws == Node.SIGNAL)
            return true;
        // 前一个节点的状态大于0,已取消状态
        if (ws > 0) {
            // 把前面全部取消状态的节点都从链表中删除
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 前一个Node的状态小于等于0,则把其状态设置为等待唤醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

在看完获取锁的流程,那么你知道ReentrantLock如何实现公平锁了吗?其实就是在tryAcquire()的实现中。

ReentrantLock如何实现公平锁?

tryAcquire()的实现中使用了hasQueuedPredecessors()保证了线程先进先出FIFO的使用锁,不会产生"饥饿"问题,

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 状态变量的值为0,说明暂时尚未线程占有锁
    if (c == 0) {
        // hasQueuedPredecessors()保证了不管是新的线程仍是已经排队的线程都顺序使用锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
          ....
        }
        ...
    }  
}
public final boolean hasQueuedPredecessors() {  
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

tryAcquire都会检查CLH队列中是否仍有前驱的元素,若是仍然有那么继续等待,经过这种方式来保证先来先服务的原则。

那这样ReentrantLock如何实现可重入?是怎么重入的?

ReentrantLock如何实现可重入?

其实也很简单,在获取锁后,设置一个标识变量为当前线程exclusiveOwnerThread,当线程再次进入判断exclusiveOwnerThread变量是否等于本线程来判断.

protected final boolean tryAcquire(int acquires) {
  
    // 状态变量的值为0,说明暂时尚未线程占有锁
    if (c == 0) {
         if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 当前线程获取了锁,并将本线程设置到exclusiveOwnerThread变量中,
            //供后续本身可重入获取锁做准备
            setExclusiveOwnerThread(current);
            return true;
        }
    } //之因此说是重入锁,就是由于在获取锁失败的状况下,还会再次判断是否当前线程已经持有锁了
    else if (current == getExclusiveOwnerThread()) {
        ...
    }
   
}

当看完公平锁获取锁的流程,那其实咱们也了解非公平锁获取锁,那咱们来看看。

ReentrantLock公平锁模式与非公平锁获取锁的区别?

其实非公平锁获取锁获取区别主要在于:

  • 构建函数中传入false或者为null,为建立非公平锁NonfairSync,true建立公平锁,
  • 非公平锁在获取锁的时候,先去检查state状态,再直接执行aqcuire(1),这样能够提升效率,

    final void lock() {
                if (compareAndSetState(0, 1))
                    //修改同步状态的值成功的话,设置当前线程为独占的线程
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    //获取锁
                    acquire(1);
            }
  • tryAcquire()中没有hasQueuedPredecessors()保证了不管是新的线程仍是已经排队的线程都顺序使用锁。

其余功能都相似。在理解了获取锁下,咱们更好理解ReentrantLock::unlock()锁的释放,也比较简单。

ReentrantLock::unlock()释放锁,如何唤醒等待队列中的线程?

  • 释放当前线程占用的锁

    protected final boolean tryRelease(int releases) {
        // 计算释放后state值
        int c = getState() - releases;
        // 若是不是当前线程占用锁,那么抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            // 锁被重入次数为0,表示释放成功
            free = true;
            // 清空独占线程
            setExclusiveOwnerThread(null);
        }
        // 更新state值
        setState(c);
        return free;
    }
  • 若释放成功,就须要唤醒等待队列中的线程,先查看头结点的状态是否为SIGNAL,若是是则唤醒头结点的下个节点关联的线程,若是释放失败那么返回false表示解锁失败。

    • 设置waitStatus为0,
    • 当头结点下一个节点不为空的时候,会直接唤醒该节点,若是该节点为空,则会队尾开始向前遍历,找到最后一个不为空的节点,而后唤醒。
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;//这里的s是头节点(如今是头节点持有锁)的下一个节点,也就是指望唤醒的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); //唤醒s表明的线程
}

综合上面的ReentrantLock的可重入,可实现公平非公平锁的特性外,还具备哪些特性?

ReentrantLock除了可重入还有哪些特性?

  • 支持线程中断,只是在线程上增长一个中断标志interrupted,并不会对运行中的线程有什么影响,具体须要根据这个中断标志干些什么,用户本身去决定。好比,实现了等待锁的时候,5秒没有获取到锁,中断等待,线程继续作其它事情。
  • 超时机制,在ReetrantLock::tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内若是获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。

ReentrantLock与Synchrionized的区别

  • ReentrantLock支持等待可中断,能够中断等待中的线程
  • ReentrantLock可实现公平锁
  • ReentrantLock可实现选择性通知,便可以有多个Condition队列

ReentrantLock使用场景

  • 场景1:若是已加锁,则再也不重复加锁,多用于进行非重要任务防止重复执行,如,清除无用临时文件,检查某些资源的可用性,数据备份操做等
  • 场景2:若是发现该操做已经在执行,则尝试等待一段时间,等待超时则不执行,防止因为资源处理不当长时间占用致使死锁状况
  • 场景3:若是发现该操做已经加锁,则等待一个一个加锁,主要用于对资源的争抢(如:文件操做,同步消息发送,有状态的操做等)
  • 场景4:可中断锁,取消正在同步运行的操做,来防止不正常操做长时间占用形成的阻塞
各位看官还能够吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
欢迎关注公众号【Ccww技术博客】,原创技术文章第一时间推出

img

相关文章
相关标签/搜索