面试准备之ReentrantLock之源码分析

目录

1.AQS

2.ReentrantLock经典题

3.ReentrantLock源码分析

3.1ReentrantLock结构图​

3.2ReentrantLock的构造方法

3.3获取锁lock()方法

3.4 释放锁

4.总结ReentrantLock的基本流程:​

5.ReentrantLock公平锁

6.ReentrantLock和Synchronized的共同点和不同点


ReentrantLock作为Java中除了synchronized之外用的最多的锁。ReentrantLock是利用AQS框架实现的乐观锁,在介绍ReentrantLock之前先看一下AQS也就是AbstractQueuedSynchronizer。

1.AQS

AQS是一个同步框架用来控制多线程访问共享资源。使得多线程有顺序的去访问共享资源。AQS中有两个很重要的元素

A.volatitle修饰状态值state,线程通过CAS操作来改变state的值来作为这个线程是否获取到锁的依据。一般都是默认state等于0的时候,这个时候锁是没有被其他线程获取的,通过CAS操作改变了state的值,如果改变成功说明获取到了锁,如果改变不成功,说明没有获取到锁

B.同步FIFO队列,当线程没有成功获取锁的时候,会将这个线程加入到队列中,然后当线程释放锁后,会从队列中唤醒一个线程重新占有锁。这个队列是一个双向队列,但这个队列不是一个真实声明的队列,它是通过一个个的Node组成的,每个Node都是对没有获取到锁的线程的封装,而且Node是有指向它前面和后面Node的指针,这样多个Node组合在一起就形成了一个双向队列,这个Node是AbstractQueuedSynchronizer里面的内部类。队列的结构图
在这里插入图片描述
Node对象的代码

static final class Node {
        //标记表示节点正在共享模式中等待
        static final Node SHARED = new Node();
        //标记表示节点正在独占模式下等待
        static final Node EXCLUSIVE = null;
        //waitStatus值表示线程已取消
        static final int CANCELLED =  1;
        //waitStatus值表示后继者的线程需要被唤醒
        static final int SIGNAL    = -1;
        //waitStatus值表示线程正在等待条件
        static final int CONDITION = -2;
        //waitStatus值表示下一个acquireShared应无条件传播
        static final int PROPAGATE = -3;
        
        volatile int waitStatus;
       // 当前节点的前驱节点
        volatile Node prev;
	   // 当前节点的后续节点
        volatile Node next;
      // 当前节点指向的线程
        volatile Thread thread;

      // nextWaiter是“区别当前CLH队列是 ‘独占锁’队列 还是 ‘共享锁’队列 的标记”
     // 若nextWaiter=SHARED,则CLH队列是“共享锁”队列;
     // 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),则CLH队列是“独占锁”队列。
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         * 返回前驱节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

  		// Used by addWaiter
        Node(Thread thread, Node mode) {   
            this.nextWaiter = mode;
            this.thread = thread;
        }
		 // Used by Condition
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

用一句话总结AQS框架就是线程通过CAS操作来改变state的值,如果成功说明这个线程拿到锁了,如果不成功就放入队列中挂起,等待它的前一个线程来唤醒它重新对state进行CAS操作来获取锁

2.ReentrantLock经典题

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author wangbiao
 * @Date 2019-11-17 16:59
 * @Decripition TODO
 **/
public class ReetrantLockTest {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    public void printA(){

        try{
            lock.lock();
            for(int i=0;i<10;i++){

                    System.out.println(Thread.currentThread().getName()+"打印A");
                    conditionB.signal();
                    conditionA.await();

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }


    }

    public void printB(){
        try{
            try{
                lock.lock();
                for(int i=0;i<10;i++){

                        System.out.println(Thread.currentThread().getName()+"打印B");
                        conditionC.signal();
                        conditionB.await();

                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public void printC(){
        try{
            try{
                lock.lock();
                for(int i=0;i<10;i++){

                    System.out.println(Thread.currentThread().getName()+"打印C");
                    conditionA.signal();
                    conditionC.await();

                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        ReetrantLockTest test = new ReetrantLockTest();
        Thread threadA = new Thread(){
            public void run(){
                test.printA();
            }
        };
        Thread threadB = new Thread(){
            public void run(){
                test.printB();
            }
        };
        Thread threadC = new Thread(){
            public void run(){
                test.printC();
            }
        };
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();

    }


}

最后输出:

Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C

3.ReentrantLock源码分析

3.1ReentrantLock结构图
在这里插入图片描述
 

ReentrantLock实现了Lock接口,并且内部有三个内部类Sync,NonFairSyn和FairSync,这里运用了模板模式,在AbstractQueuedSynchronizer里面实现了加锁和释放锁的抽象操作,就是大体的操作流程,但是具体实现例如tryAcquire,tryRelease这些方法就要靠具体的实现类来实现了,我觉得还用到了适配器模式,通过实现Lock接口,但在实现方法里面引用了Sync的具体方法。


3.2ReentrantLock的构造方法

可以发现ReentrantLock默认的是非公平锁,如果在申明ReentrantLock的时候,向构造器里面传入一个true,那ReentrantLock就是一个公平锁

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
   // 同步器的引用
    private final Sync sync;
   //非公平锁的构造函数
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // 公平锁的构造函数
	public ReentrantLock(boolean fair) {
	// 三目运算符,如果为true 则为公平锁,反之为非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

3.3获取锁lock()方法

public void lock() {
        sync.lock();
    }

可以看到lock方法实际调用的是NonFairSync里面的lock方法,先直接利用CAS操作尝试去改变state的值,如果成功,就说明获取到了锁,如果没有成功再调用acquire,公平锁的lock方法和这里不太一样,公平锁的lock不会直接去尝试改变state的值,而是直接调用acquire方法。
NonFairSync.lock方法

final void lock() {
        //尝试直接改变state的值,如果成功说明获取到锁
        if (compareAndSetState(0, 1))
            //将锁的独占线程设置为当前线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //如果直接尝试改值失败,就进行下面的操作
            acquire(1);
    }

AbstractQueuedSynchronizer.acuqre方法

 /**
     * tryAcquire(arg)方法
     * 1.尝试通过CAS操作改变state值来拿到锁
     * addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE)
     * 2.操作失败就将线程封装成Node并放入队尾
     *  acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)
     * 3。放入到队尾后判断上一个节点是否是队头,如果是的,就尝试获取锁,获取失败就挂起
     * 如果不是队头就挂起
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
            //如果没有获取到锁,但是有中断操作,就自己中断
            selfInterrupt();
    }

到回到NonFairSync的tryAcquire方法,先判断state是否等于0,等于0说明当前锁没有被占用,利用CAS操作更改state的值,要是更改成功说明获取锁成功,如果state不等于0,就判断占有锁的线程是不是当前线程,要是是的,就对state进行加一操作。

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
​
/**
     * 先尝试利用CAS操作更改state的值,如果更改成功,说明拿到锁了,
     * 如果不成功,说明这个锁已经有线程占有了,那就判断占有这个锁的线程是否是当前线程
     * 如果是的,state值进行加一操作,这里也体现了ReentrantLock是可重入锁
     * @param acquires
     * @return
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        //获取当前锁的状态值,如果是等于0,说明没有线程占有这个锁
        int c = getState();
        if (c == 0) {
            //通过cas改变状态值,如果更改成功,说明取得了锁
            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;
    }

​

AbstractQueuedSynchronized的addWaiter方法,这个方法就是将当前线程封装成Node,并快速添加到队尾,如果快速添加失败,就通过for的无限循环插入到队尾

private Node addWaiter(Node mode) {
        //先将当前线程封装成Node,由于mode是null,所以是独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 获取队尾,通过cas快速替换当前tail节点为node节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果快速插入失败,则利用无限for循环进行插入操作
        enq(node);
        return node;
    }
/**
     * 一种常见的CAS操作,就是无限for循环,直到CAS操作成功才跳出for循环
     * @param node
     * @return
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果队尾是空的,就先初始化一个节点,并设置为头节点,队尾也指向头节点
            //然后执行完后再循环一次,队尾就不会为空了
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //将node设置为队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

成功将Node插入到队尾后,判断这个队尾的前一个Node是不是队头,如果是队头的话尝试获取锁,如果它前面的节点不是队头的话就挂起,等待被唤醒后再次执行判断它前面的Node是不是队头,如果是队头就尝试获取锁,如果获取成功,就将这个节点设置成头节点。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环,直到满足条件退出
            // 首先将当前线程进行阻塞,等待其他线程进行唤醒
            // 其他线程进行唤醒以后,判断当前是否获取资源。此时,可能有其他线程的加入,导致获取失败
            for (;;) { 
            // 获取当前节点的前驱节点
                final Node p = node.predecessor();
            // 如果当前节点的前驱节点是head节点,且尝试获取锁成功
                if (p == head && tryAcquire(arg)) {
                // 设置当前的node节点为head节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 判断当前线程是否应该阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果当前线程被中断,则 parkAndCheckInterrupt()返回为true。interrupted = true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 /**
     * 如果Node的前节点的waitStatus是等于1的,那么就直接可以确认将Node挂起的
     * 如果waitStatus是大于0的,则代表线程被取消,重新设置Node的前驱节点
     * 找到第一个ws<=0的节点,将这个节点设置为Node的前驱节点
     * 默认的Node的ws是null的,所以会将ws设置为SIGNAL,也就是-1
     *
     * @param pred
     * @param node
     * @return
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

将当前线程挂起,挂起后如果被唤醒,就判断当前线程是否被中断了

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

总结一下ReentrantLock非公平锁的获取:

1.尝试通过cas操作来改变state的值,如果能改变则获取到锁。

2.如果没能改变state的值,则判断state的值是否为0,如果为0则尝试用cas操作来改变state值来拿锁。

3.如果state的值不为0,则判断拿到锁的线程是否是自己,如果是的,则state加一操作,如果获取锁的线程不是自己,则将自己封装成一个node节点,放入到队尾。

4.放入队尾后,判断自己是否是头节点,如果是头节点,则尝试改变state的值获取锁,如果不是头节点,则挂起来,等待前面的节点来唤醒。

3.4 释放锁


调用的AbstractQueuedSynchronizer的release方法,

public void unlock() {
        sync.release(1);
    }
/**
     * 如果释放资源成功,并且头节点不为空,并且头节点的状态值不等于0,
     * 就唤醒下一个Node节点来获取锁
     * @param arg
     * @return
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease方法,锁的状态值进行减一操作,如果状态值等于0,就说明这个锁被完全释放

/**
     * 先对锁的状态值进行减一操作,如果当前线程不是获取到锁的线程,就抛出IllegaMonitorStateException异常
     * 如果状态值在减一后变成0,那么说明这个锁被完全释放了,将占有锁的线程设置成空
     * @param releases
     * @return
     */
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

释放完锁后就需要唤醒下一个节点来获取锁,AbstractQueuedSynchronizer的unparkSuccessor

private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {
        //把状态值设为0
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /**
         * 获取Node的后驱元素,如果后驱节点是空,或者状态是取消,
         * 要找到离队头最近的状态是小于等于0的节点,然后就将这个节点唤醒
         */
        Node s = node.next;
        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);
    }

4.总结ReentrantLock的基本流程:
这里写图片描述

这里是对于非公平锁而言的流程
加锁的过程
A.直接利用CAS更改同步状态值,如果成功,就将锁的独占线程设置成当前线程,说明获取到锁
B.更改状态值失败,在tryAcquire方法里面获取状态值,如果状态值等于0,就利用CAS操作更改状态值,成功说明获取到锁
   如果状态值不等于0,判断拥有锁的线程是不是当前线程,如果是的,就对状态值加一操作,拿到锁
C.B过程中没有拿到锁,就将当前线程封装成Node节点,并快速插入到队尾,如果快速插入失败的话,要是队头为null的话,先初始化一个Node节点作为队尾,再然后将Node节点作为队尾插入。
D.插入队尾完成后,利用无限for循环,先判断当前Node节点的前驱节点是否是队头,如果是的,尝试获取锁,如果获取成功就直接返回,如果失败或者前驱节点不是队头,判断自己的前驱节点的状态是否是SIGNAL,SIGNAL代表可以唤醒后驱节点,如果是的直接返回true,如果不是的,就找到最近的一个状态是SIGNAL的节点作为前驱节点返回true,再将自己挂起,等待唤醒。
E.挂起后,如果被唤醒,判断自己是否被中断了,如果被中断就自己尝试中断自己。

解锁的过程
A.更改状态值也就是减一操作,判断当前线程是否是持有锁的线程,如果是的,则判断释放后的状态值是否为0,如果是的说明锁被完全释放,返回true,如果锁没有被完全释放,返回false
B.如果锁被完全释放,就唤醒队头的后驱节点,唤醒的前提是后驱节点的waitstatus是小于等于0的,如果不是的,找到离队头最近的waitstatus是小于等于0的节点进行唤醒,唤醒调用LockSupport.unpark(s.thread);

5.ReentrantLock公平锁


ReentrantLock默认的是非公平锁,当申明ReentrantLock实例的时候向构造函数里面传参数true的时候,就是公平锁了,非公平锁和公平锁最多的区别是在都调用lock方法的时候:
非公平锁调用lock方法
首先是直接去更改锁的状态值来尝试获取锁,如果获取失败了,再进行下面的操作

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

公平锁加锁的操作:

final void lock() {
            acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
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;
        }
    }

在FairSync重写tryAcquire方法的时候,去获取锁的前面加了一个hasQueuedPredecessors的判断,这个判断是判断队列中有没有比自己排在更前面的节点,也就是头节点的下一个节点是否存在。

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

总结一下公平锁和非公平锁在获取锁的过程中有什么不同:
非公平锁加锁的时候是直接先去利用CAS操作更改锁的状态值来判断有没有拿到锁,如果没有拿到,再去判断锁的状态值是否等于0,如果等于0再去尝试获取锁。
公平锁加锁的时候会先去获取锁的状态值是否等于0,如果等于0,还要去判断当前队列中是否有排在自己前面的节点,如果没有才去获取锁。这也是在新东方面试的时候没有回答上来的问题。


6.ReentrantLock和Synchronized的共同点和不同点


共同点:
两者都是可重入的独占锁,ReentrantLock在默认的情况下是非公平锁,Synchronized也是非公平锁
不同点:
1.Synchronized是在字节码指令进行加锁操作,ReentrantLock实在代码层面进行加锁操作
2.Synchronized锁的释放在执行完同步代码块或者同步方法的时候就会自动释放,ReentrantLock加完锁后得调用unlock方法手动释放,所以一般将unlock方法在finally里面来释放的
3.线程在获取Synchronized的同步锁的时候如果被阻塞了,会一直阻塞,不能被中断,不能干别的,而ReentrantLock在加锁的时候选择tryLock加锁,可以在没有获取到锁后直接返回,不用阻塞,或者lockInterruptibly方法加锁的话,可以在阻塞的过程中被中断
4.如果在加锁的代码块中运行发生异常,Synchronized会释放锁,ReentrantLock不会释放锁,所以一般将ReentrantLock锁的释放放在finally代码块里面。
5.ReentrantLock结合condition使用会比Synchronized和wait,notify结合使用的话更加灵活,ReentrantLock可以结合condition来唤醒和锁定特定的线程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章