关于lock:面试为了进阿里需要深入理解ReentrantLock原理

45次阅读

共计 6698 个字符,预计需要花费 17 分钟才能阅读完成。

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

前言

在面试,很多工夫面试官都会问到锁的问题,ReentrantLock 也是常问一个点,但具体会问什么呢? 在网上收集到一些问题:

  • 重入锁是什么?
  • 偏心锁和非偏心锁是什么?有什么区别?
  • ReentrantLock::lock 偏心锁模式事实

    • 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 {}

那偏心锁和非偏心锁是什么?有什么区别?

那偏心锁和非偏心锁是什么?有什么区别?

偏心锁是指当锁可用时, 在锁上等待时间最长的线程将取得锁的使用权,即先进先出。而非偏心锁则随机调配这种使用权,是一种抢占机制,是随机取得锁,并不是先来的肯定能先失去锁。

ReentrantLock 提供了一个构造方法,能够实现偏心锁或非偏心锁:

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

// 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 技术博客】,原创技术文章第一工夫推出

正文完
 0