ReentrantLock 加锁过程源码详解

ReentrantLock 是什么

是java 实现的公平锁/非公平锁,也是可重入锁

跟aqs (AbstractQueuedSynchronizer ) 什么关系

AbstractQueuedSynchronizer 翻译过来是抽象队列同步 是不是意味着aqs内部维护中一个队列。 1.下图是 ReentrantLock 内部实现 公平锁FairSyncaqs的关系

 

ReentrantLock 加锁过程源码详解_第1张图片

 

 

2.下图是 ReentrantLock 内部实现 公平锁NonfairSyncaqs的关系

 

ReentrantLock 加锁过程源码详解_第2张图片

 

 

3.aqs 内部维护的队列是什么样子的

简单看下aqs内部队列的实现

static final class Node {
//该节点的上一个节点
    volatile Node prev;
//该节点的下一个节点
    volatile Node next;
//持有该节点的线程    
     volatile Thread thread;
}

可以看出来aqs内部维护着一个Node 有点像LinkedList,所以aqs底层准确来说是由一个链表实现的

4.aqs 出了有个内部类是Node外,aqs这个类属性还维护着链表的头节点尾节点

//头节点
    private transient volatile Node head;
//尾节点
    private transient volatile Node tail;

思考

  • ReentrantLock 是怎么锁住的?有什么标识说已经上锁了嘛
  • ReentrantLock 等待获取锁是怎么实现的,等待的线程在干嘛,之后怎么获取到锁
  • ReentrantLock 可重入锁怎么实现的
  • ReentrantLock 公平锁跟公平锁的实现有什么不同

带着问题看源码

以公平锁为列

简单示例

    public static void main(String[] args) {
    //ReentrantLock 默认是非公平锁, new ReentrantLock (true);
    //构造方法传入 true 表示 公平锁
        ReentrantLock lock = new ReentrantLock (true);
        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println("t1 lock ing");
            Thread.sleep(100000000);
            lock.unlock();
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            lock.lock();
            System.out.println("t2 lock ing");
            Thread.sleep(100000000);
            lock.unlock();
        });
        t2.start();
        Thread t3 = new Thread(() -> {
            lock.lock();
            System.out.println("t3 lock ing");
            Thread.sleep(100000000);
            lock.unlock();
        });
        t3.start();
    }
    //true 表示公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock 上锁过程

以上面的demo为列子,分为三种常见

直接到公平锁的实现 lock方法,直接调用 acquire(1) 注意 参数是 1 ,这个参数什么意思?表示上锁一次

        final void lock() {
            acquire(1);
        }

acquire()

// 此时 arg = 1
    public final void acquire(int arg) {
    //1.tryAcquire 尝试加锁,当返回false也就是加锁失败执行
    //acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    //2.addWaiter(Node.EXCLUSIVE) 加锁失败之后,将该节点添加到等待队列
    //3.再次尝试获取锁,获取不到锁就等待
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //aqs的方法,aqs的属性 state 此时0表示没有加锁状态
            //state>0 表示上锁的次数(重入锁会大于1)
            int c = getState();
            //此时t1过来,第一次,所以肯定是0,锁处于自由状态
            if (c == 0) {
            //1.判断队列是否还有在排队的,如果没有走 compareAndSetState
            //2.compareAndSetState 比较交换state 这就是上锁,通过cas修改state的值
            //3.setExclusiveOwnerThread 设置当前持有锁的线程
            //exclusiveOwnerThread 这个属性是aqs维护的表示当前持有锁的线程对象
            //4.t1过来 hasQueuedPredecessors = false 因为当前队列中没有节点
            //所以就执行2跟3获取到锁,但是假如现在t1获取到锁,t2过来了 这个if就进不去了
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //t2上面的if进不去,就会走这个if
            //current == getExclusiveOwnerThread()  t2的线程跟当前持有锁的线程肯定不
            //相等,因为现在持有锁的线程是t1
            //所以这个if什么时候会进入呢,这个就是可重入锁的原理
            //当t1在持有锁的时候再次调用lock此时线程=当前持有锁的线程
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
              //设置state 也就是往sate加1
                setState(nextc);
                return true;
            }
            return false;
        }
    }

总结tryAcquire 1.会尝试获取锁,通过判断当前队列头节点是否是当前想要获取锁的线程,如果是就尝试获取锁。2.如果获取到锁就做两件事情,a.通过cas修改state标志为上锁 b.修改aqs当前持有锁的线程对象 3.判断是否重入锁,如果重入锁通过casstate加一 4.如果获取到锁返回true否则返回false

addWaiter(Node.EXCLUSIVE)

当获取不到锁就会走addWaiter(Node.EXCLUSIVE)的逻辑,所以t1是不会走addWaiter(Node.EXCLUSIVE) t2,t3就会走这个逻辑

 private Node addWaiter(Node mode) {
 //新建一个持有当前线程的节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
 //tail是尾节点,没有初始化,所以t2进来的时候tail = null,所以t2不走下面逻辑
 //但是t3过来的时候,tail已经被t2初始化了(下面enq做的)所以t3会走下面的逻辑
        if (pred != null) {
        //t3过来此时尾节点是t2,设置t3节点的前节点为t2
            node.prev = pred;
            //通过cas修改t3的node为尾节点
            if (compareAndSetTail(pred, node)) {
            //将t2的下一个节点设置成t3,到这里一个链表就形成了
            //head node -> node(t2)->tail node(t3)
                pred.next = node;
                return node;
            }
        }
    //t2会走这里,t1会加锁成功,不会走这个,t3在上面形成链表之后return出去了
        enq(node);
        return node;
    }

enq(node)

    private Node enq(final Node node) {
    //死循环
        for (;;) {
        //t2第一次循环的时候 tail = null
            Node t = tail;
            if (t == null) { // Must initialize
            //cas 设置 head = new Node()
            //tail = head 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            //t2进来第二次循环,这时候tail = head 
            //所以走这个逻辑
                node.prev = t;
            //cas将t2设置成tail,head的next指向t2  head ->node(t2)
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

总结:1.当t2进来addWaiter的时候,tail=head=null,所以会走enq方法去初始化tail=head=new Node(),将head.next = Node(t2),此时的head其实是空的,就是一个摆设 2.当t3过来 addWaiter 的时候 因为t2初始化了tail所以走if的逻辑,设置Node(t3)= tail,形成 head Node -> Node(t2)->tail Node(t3)的链表 3.注意链表是双向的 4.到这里已经将t2,t3加入链表也就是aqs的队里

acquireQueued

  final boolean acquireQueued(final Node node, int arg) {
  //是否失败的标识
        boolean failed = true;
        try {
        //线程是否被打断的标识
            boolean interrupted = false;
        //又是死循环    
            for (;;) {
            //拿该节点的上一个节点
                final Node p = node.predecessor();
            //判断该节点上一个节点是否头节点,如果是头节点就tryAcquire尝试获取锁
            //t2过来的时候 刚好满足所以p==head 所以会走 tryAcquire 尝试获取锁
            //但是现在锁被t1持有,获取失败,走下面的逻辑
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
            //t2和t3都会走这个逻辑
            //1.shouldParkAfterFailedAcquire 判断是否可以park线程
            //通过该节点的上一个节点的 waitStatus == -1 表示该节点可以park
            //waitStatus初始化等于零,所以刚开始进去 shouldParkAfterFailedAcquire 这个方法
            //会把上一个节点的 waitStatus 改成-1,下一次循环走到这里满足条件了
            //2.parkAndCheckInterrupt 线程park,也就是暂停线程,等待获取锁资源
            //3.后续解锁就从这里开始走
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

总结:到这里 ReentrantLock 加锁过程大概完了,acquireQueued这个方法主要是再去尝试获取锁,如果获取不到锁,就将线程park。这里三个细节点 1.什么时候会走tryAcquire再次尝试获取锁,当上一个节点是头节点的时候 2.线程park的条件是什么,是上一个节点的 waitStatus = -1 3.上一个节点的 waitStatus 什么时候会变为-1,一开始时候是0,当进入acquireQueued的死循环,第一次shouldParkAfterFailedAcquire 在这个方法把 waitStatus 改为-1

解决问题

  • ReentrantLock 是怎么锁住的? 是通过aqs的state属性来标识的,=0标识没有被持有锁,大于1标识被持有锁
  • ReentrantLock 等待获取锁是怎么实现的,等待的线程在干嘛,之后怎么获取到锁?线程会先在去尝试获取锁,获取不到就会进行park,之后被unpark然后重新走 acquireQueued for循环去尝试获取锁
  • ReentrantLock 可重入锁怎么实现的?aqs的state值大于1,解锁的时候也会有对应的操作
  • ReentrantLock 公平锁跟公平锁的实现有什么不同?非公平锁一进去lock方法就会调用compareAndSetState 去尝试获取锁。

非公平锁

图解t1,t2,t3 队列情况

t1

t1得时候head=tail=null,获取锁之后修改stateexclusiveOwnerThread

 

ReentrantLock 加锁过程源码详解_第3张图片

 

 

t2

t2得时候锁还没释放,新建一个Node = head = tail,新建自己得Node t2Node,此时head.next = t2Node

ReentrantLock 加锁过程源码详解_第4张图片

 

 

t3

t3来的时候锁还没释放,新建t3Nodet3Node.pre = t2Node tail = t3Node,同时修改t2Node得waitStatus 表示t3可以park

ReentrantLock 加锁过程源码详解_第5张图片

 

 

图解Lock代码时序图

  • 主流程

    ReentrantLock 加锁过程源码详解_第6张图片

     

  • tryAcquire 尝试加锁流程

 

ReentrantLock 加锁过程源码详解_第7张图片

 

 

  • tryAcquire 重入锁

    ReentrantLock 加锁过程源码详解_第8张图片

     

  • addWaiter 添加到队列里面

 

ReentrantLock 加锁过程源码详解_第9张图片

 

 

  • acquireQueued 自旋,park 线程,等待被唤醒

 

ReentrantLock 加锁过程源码详解_第10张图片

 

 

未完待续...........


作者:三餐吃到饱1
链接:https://juejin.im/post/5f14f3336fb9a07ea929edad

你可能感兴趣的:(ReentrantLock 加锁过程源码详解)