带你一行一行代码分析ReentrantLock 的原理

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


AQS原理

概述

全称是AbstractQueuedStnchronizer,是阻塞式锁和相关同步器工具的框架

特点:
1、用state属性来表示锁的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

  • getstate:获取state状态
  • setState:设置state状态
  • compareAndSetState:乐观锁机制设置state状态

2、提供了FIFO的等待队列,类似Monitor的EntryList
3、条件变量来实现等待、唤醒机制、支持多个条件变量。类似于Monitor的WaitSet

子类主要实现这样一些方法(默认抛出UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleasedShared
  • isHeldExclusively

获取锁的姿势

//如果获取锁失败
if(!tryAcquire(arg){
   //入队,可以选择阻塞当前队列   park unpark
}

释放锁的姿势

//如果获取锁失败
if(tryRelease(arg){
   //让阻塞线程恢复运行
}

AQS其实定义的是实现锁的原理,下面是我自己写的一个基于装饰者模式不可重入锁的实现示例

/自定义锁,不可重入锁
class Mylock implements Lock {
    //独占锁
    class Mysync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                //加上了锁,设置owner为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            //  state是 volatile修饰的,这里最好把setState写在setExclusiveOwnerThread后面
            setState(0);
            //写屏障可以保证Owner的修改也会被同步到主存,对其他线程可见
            return true;

        }

        //是否持有独占锁
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;

        }

        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private Mysync sync = new Mysync();


    //加锁,不成功进入队列等待
    @Override
    public void lock() {
        sync.acquire(1);
    }

    //加锁,可打断
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //尝试加锁
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //尝试加锁 超时
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    //释放锁
    @Override
    public void unlock() {
        sync.release(1);
    }

    // 条件变量
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

测试一下

@Slf4j(topic = "c.TestAqs")
public class TestAqs {
    public static void main(String[] args) {

        Mylock lock = new Mylock();
        new Thread(()->{
            lock.lock();
            try {
                log.debug("locking");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                log.debug("unlocking");
                lock.unlock();
            }
        },"t1").start();

        new Thread(()->{
            lock.lock();
            try {
                log.debug("locking");
            } finally {
                log.debug("unlocking");
                lock.unlock();
            }
        },"t2").start();

    }

}
19:01:14.944 [t1] DEBUG c.TestAqs - locking
19:01:15.967 [t1] DEBUG c.TestAqs - unlocking
19:01:15.968 [t2] DEBUG c.TestAqs - locking
19:01:15.968 [t2] DEBUG c.TestAqs - unlocking

ReentrantLock 原理

在这里插入图片描述
由类图可以看出,ReentrantLock 继承自Lock,也提供了以sync的同步器,这个同步器有两种模式,一个是公平模式,一个是非公平模式,同时这个同步器也是基于AQS原理

加锁流程

先从构造器看,默认为非公平锁实现

 public ReentrantLock() {
        sync = new NonfairSync();
 }

NonfairSync继承自AQS
在这里插入图片描述
在这里插入图片描述

也就是说同步器有两种实现,一种是公平的,一种是非公平的,公平的满足先进先出(FIFO)

加锁成功

首先看下加锁 lock()源码

  abstract void lock();

它有两种实现,一种是公平的,一种是非公平的
在这里插入图片描述
1、非公平

 final void lock() {
            //先尝试用compareAndSetState修改state,如果修改成功就是加锁成功
            if (compareAndSetState(0, 1))
                //加锁成功,把owner线程(exclusiveOwnerThread)改为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//如果出现竞争,比如state已经被修改为1,当前线程加锁失败,执行此方法
        }

代码执行流程:

  • 如果CAS修改state的值为1成功,就意味着加锁成,然后修改exclusiveOwnerThread为当前加锁的线程
  • 如果执行失败执行acquire(int arg)方法

如果state是0的话,,没有其他线程与当前线程竞争,那么势必加锁成功

在这里插入图片描述

2、公平

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

代码执行流程: 直接执行 acquire(1)方法

3、acquire(int arg)

有以上公平锁和非公平锁的加锁可以看出,非公锁加锁失败后执行acquire方法,公平锁直接执行acquire,acquire方法是独占式的获取锁的流程,我们就对这个方法进行分析

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

代码执行流程:
首先执行tryAcquire(int arg) 方法

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

(1)tryAcquire(int arg) 非公平锁的实现

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

 final boolean nonfairTryAcquire(int acquires) {
            //  获取当前线程对象current
            final Thread current = Thread.currentThread();
          //   获取state的值
            int c = getState();
         
          //  很多人可能会疑惑,之前非公平的加锁lock() 不是加锁失败了嘛,怎么这里还判断,这是因为
          //  在从lock执行到nonfairTryAcquire方法时,可能有加锁线程已经释放了锁
            
            
            if (c == 0) {
           //  如果没线程没有加锁。当前线程直接CAS更改state,CAS操作成功意味着加锁成功
          //   需要修改exclusiveOwnerThread为当前线程,并返回true,意味着加锁成功
                if (compareAndSetState(0, acquires)) 
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
        
            else if (current == getExclusiveOwnerThread()) {
              //   如果已经有线程加锁,就判断加锁的这个线程是不是当前线程,如果是就把state+1,
              //   意味着锁重入行为发生,并返回ture
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

(2)tryAcquire(int arg) 公平锁的实现

 protected final boolean tryAcquire(int acquires) {
           //  同样获取当前线程对象current
            final Thread current = Thread.currentThread();
              //   获取state的值
            int c = getState();
              //   判断是否加锁(不再解释)
            if (c == 0) {
           
            //    不同的是这一点,公平锁在加锁前会通过hasQueuedPredecessors()方法,判断同步队列中是不是有其他节点,
             //   这是因为公平锁要满足先进先出的特性,要让其他等待时间长的先加锁
                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;
        }
    }

小结:分析了非公平锁和公平锁的加锁流程,可以得知,

  • 非公平锁在加锁的时候,不需要判断同步队列中是否有其他节点,只要CAS操作更改state成功了,就意味着当前线程获取到了锁
  • 公平锁在加锁的时候,需要判断同步队列中是否有其他节点,也就是先让head节点的后继结点先加锁,以满足先进先出的特性
  • 所谓的锁重入,也不过是判断是否是当前线程,如果是当前线程就state+1,实现加锁的重入操作

值得说的是,个人建议,如果没有特殊需求,请用非公平锁,虽然非公平锁可能会发生线程·饥饿,但是非公平锁会带来更少的上下文切换,吞吐量更好

加锁失败

我们之前分析的加锁,都是加锁成功的情况,如果加锁失败呢
还是下面这块代码

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

(1) 首先执行addWaiter,构建同步队列

private Node addWaiter(Node mode) {
         //以当前线程创建节点node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //如果是第一次添加节点,tail是null执行enq方法
        Node pred = tail;
        if (pred != null) {
           // 把node节点加入队列尾部
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }


private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上面enq就是一个死循环形成同步队列的过程,可以自己画图一步一步分析,执行完enq,就是下图这么一个队列
在这里插入图片描述

(2) 然后执行acquireQueued

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //获取当前节点的前驱节点
                if (p == head && tryAcquire(arg)) {//如果前去节点是head,就尝试获取锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire判断获取锁失败的线程是否应该park住,
               // 第一次执行时,当前节点的前驱节点waitStatus是默认值0(刚加入同步队列的都是0),
               // 根据下面shouldParkAfterFailedAcquire源码可以得知,该方法会把waitStatus置为-1,并返回fasle
              //因此第一循环parkAndCheckInterrupt不执行
               // 第二次执行到这shouldParkAfterFailedAcquire方法时
                // 根据源码,可知shouldParkAfterFailedAcquire会返回true
                // 因此执行parkAndCheckInterrupt方法,park住当前线程,不让它占用CPU了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

static final int SIGNAL    = -1;
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();
    }

最终第二个线程如果加锁失败就会形成这样:
第一个Node称为Dummy(哑元)或者烧饼,用来占位,不关联线程

在这里插入图片描述

小结:加锁失败的线程会被包装成Node节点,构成一个同步队列,线程刚开始会自旋在acquireQueued方法中,但是不是一直在执行死循环,而是循环两次后,被park住,第一循环是修改前驱节点的waitStatus为-1,第二次循环,判断出前驱节点的waitStatus为-1后,会park住当前线程

解锁流程

如果有多个线程加锁失败就会变成这个样子:
在这里插入图片描述
那怎么解锁呢?看下unlock()源码

 public void unlock() {
        sync.release(1);
    }

解锁最终是执行同步器的release(int arg) 方法,那就分析release(int arg)

 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(int arg)

protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

找到ReentrantLock 的实现

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//获取当前的state值并减1
            //getExclusiveOwnerThread是获取当前加锁的线程,(还记得之前exclusiveOwnerThread嘛)         如果解锁的不是解锁的线程,直接报错
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//如果state为-1后为0了,就意味着解锁成功
                free = true;
                setExclusiveOwnerThread(null);
                //修改exclusiveOwnerThread为null,当前没人占有锁
            }
            setState(c); //把state更新为c
            return free;
        }

上述代码的执行逻辑:先判断解锁的线程是不是锁拥有线程,如果不是就抛出异常,如果是,就判断state-1后,是不是为0,如果为0了,说明解锁成功,把锁的拥有者置为null。更新state的值为state-1
如果state-1不等于0.说明当前线程之前发生过重入,那么每次解锁state都减去1,直到state=0,解锁成功

再看release(int arg) ,如果解锁成就会执行if条件内的代码

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head; //获取头节点head,当前场景肯定不会为null呀,因为之前enq创建同步队列的时候,初始化了
            //判断同步队列中是不是有阻塞的线程,如果有就执行unparkSuccessor唤醒线程
            if (h != null && h.waitStatus != 0) 
            // 当前waitStatus 为-1,因为之前节点进入同步队列在自旋的时候,第一次自旋就是修改其前驱节点的waitStatus 为-1
            
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


 private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)//把head节点的waitStatus 置为0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;//获取头节点的后继结点,本例肯定不为null呀,因为线程阻塞
        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) 
            //unpark  唤醒当前线程
            LockSupport.unpark(s.thread);
    }

小结:解锁的时候,先调用tryRelease方法释放锁,如果释放锁的线程不是当前锁拥有线程直接抛出异常,如果是,当state=0时,才意味着解锁成功,解锁成功后,就会唤醒同步队列中的head节点的后继结点,唤醒后的节点会重新竞争锁,那就会执行一遍加锁逻辑。如果竞争成功就获得锁,否则加到同步队列尾部

在这里插入图片描述

可重入原理

前面分析加锁解锁的时候,也分析了可重入了
总结下来就是

  • 加锁时,如果锁已经被获取,且是当前线程获取的话,就实现锁重入,锁重入就是把state加1
  • 解锁时,如果解锁的是当前获取锁的线程,就把state-1,如果state变为0,那就是解锁成功,如果state>1,说明之前重入了多次,只有等多次执行释放锁操作之后,如果state为0,就意味锁重入的解锁成功

打断原理

ReentrantLock,如果被打断有什么反应呢?我们直到被阻塞线程如果被打断,必然会抛出InterruptedException异常

不可打断的获取锁

当前线程如果在执行lock()方法时,如果获取失败,经过上面加锁的分析,我知道线程会在自旋中进行park,也就是阻塞,

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //获取当前节点的前驱节点
                if (p == head && tryAcquire(arg)) {//如果前去节点是head,就尝试获取锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire判断获取锁失败的线程是否应该park住,
               // 第一次执行时,当前节点的前驱节点waitStatus是默认值0(刚加入同步队列的都是0),
               // 根据下面shouldParkAfterFailedAcquire源码可以得知,该方法会把waitStatus置为-1,并返回fasle
              //因此第一循环parkAndCheckInterrupt不执行
               // 第二次执行到这shouldParkAfterFailedAcquire方法时
                // 根据源码,可知shouldParkAfterFailedAcquire会返回true
                // 因此执行parkAndCheckInterrupt方法,park住当前线程,不让它占用CPU了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //打断后parkAndCheckInterrupt返回true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


  private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); //线程阻塞在这
        return Thread.interrupted();//如果被打断,返回打断标记true,另外interrupted还清除了打断标记,置为false
    }

我们看出来,被打断的线程,只不过从阻塞状态又进行了一次自旋,尝试获取锁,如果失败,还是会继续park阻塞线程

可打断的获取锁

ReentrantLock还有一个可打断的方法lockInterruptibly(),我们重点关注怎么处理打断的

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

  public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();//关键代码
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }


     private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();//关键代码
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

从源码中可以看出,如果线程被打断会直接抛出InterruptedException,同时也会修改打断标记。

总结
lock()不可打断的获取锁,如果获取锁失败的阻塞的线程被打断,不会抛出异常,而是进行一次自旋,尝试获取一次锁,如果获取不到被打断的线程还会继续阻塞
lockInterruptibly()是可以被打断的获取锁的方法,如果被打断,直接抛出InterruptedException异常,同时会修改打断标记为fasle

条件变量原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

signal流程
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

 public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

       /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

 final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值