ReentrantLock源码原理解释

ReentrantLock

原理解释

这种东西就是心中有个大概就好了,尝试看看源码也听有意思的。 多几次debug就好了
注意:这个就是一个 state 在加一个双向链表
ReentrantLock源码原理解释_第1张图片
只要记住这个情况,后面debug起来就顺利了。

     ReentrantLock lock = new ReentrantLock();
        new Thread(()->{
            lock.lock();
            try {
                System.out.println("thread 1 "+ new Date());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

打上断点,看一下是怎么运行的。

  • 首先会进入到 同步器的lock方法
 public void lock() {
        sync.lock();
    }

sync是ReentrantLock内部自己实现的内部类,继承于AbstractQueuedSynchronizer(同步器) 同步器在 JUC 包中是很重要的,在同步器中,最主要的 state,和一个双向链表,这个链表中
有一个 head 和tail,链表中的节点都是存放的都是线程. 基本上JUC包中大多数的类都是继承于它。在 AbstractQueuedSynchronizer 中并没有 规定 state 的含义。具体的实现交给子类,不同的实现
就是不同的锁。
ReentrantLock中的sync有两个实现类。都是在ReentrantLock 维护。

/**
  * Base of synchronization control for this lock. Subclassed
  * into fair and nonfair versions below. Uses AQS state to
  * represent the number of holds on the lock.
  */
 abstract static class Sync extends AbstractQueuedSynchronizer {
     private static final long serialVersionUID = -5179523762034025860L;

     /**
      * Performs {@link Lock#lock}. The main reason for subclassing
      * is to allow fast path for nonfair version.
      */
     abstract void lock();
 .......... 后面的就省略了.

看代码上面的注释就很清晰, Sync两种实现类,一种是公平,一种是非公平。 对了,Sync是abstract的。
对了 ReentrantLock 默认是非公平的。

  /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync(); //默认是给了一个 非公平的同步器
    }

在继续看lock

  /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            //尝试把 state变为 1 底层还是通过unsafe的cas操作来做的
            if (compareAndSetState(0, 1))
                //将锁的持有者变为 当前时间
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //加锁失败,竞争失败,  下面的操作简单来讲就是 构建线程节点,将这个节点添加进 队列
                acquire(1);
        }

简单来讲,加锁就是改变 ReentrantLock 里面sync同步器的state, 1 表示加锁,0表示未加锁。 在ReentrantLock 里面除了同步器之外,还有一个表示当前是哪个线程加锁了,
接下来就简单了,线程来了,就开始抢占state,谁能把state改为1,谁就占有这把锁。 抢不到的线程就在 同步器里面的线程队列里面等待,等state变为0了,按照加入的顺序,再次抢占锁。
可重入的锁的原理 还是围绕 state来做文章,如果某个线程加锁成功,那此线程就是当前线程的所有者。这个线程再次加锁的时候,先看自己是不是锁的持有者,如果是就把state加1,state
是多少就表示他加了几次锁。解锁的时候就把state-1 ,如果state变为0了就把锁的持有线程变为null,表示还没有加锁。同时看队列中有没有线程节点,有的话就唤醒它。
看看 acquire(1) 方法
这个方法是 同步器(AbstractQueuedSynchronizer)里面的

   public final void acquire(int arg) {
       /*
       再次尝试 获取锁
       构建线程node ,添加进队列。        
*/
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

看看 tryAcquire 方法
注意 tryAcquire 是抽线方法,具体的实现得看子类,这里我选择的非公平的实现(NonFairSync)

  */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

这里的就很简单了,得到当前线程,得到state,看state是否为0,如果为0尝试加锁,否则就看当前锁的持有者是不是自己,如果是就锁重入。
看看addWaiter方法
这个方法是 同步器(AbstractQueuedSynchronizer)里面的

   /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    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;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

根据上面的注释可以知道,这个方法里面就是创建了一个入队的节点。并指定这个节点的模式。
如果是第一个线程来的。pred肯定是null,那就会进入到enq()方法。
看看enq方法


    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }

请注意:这里是一个死循环,意味着这里肯定是要干成事情的,要不然就不会返回,一直干下去。
线程第一个访问的时候,tail肯定是null,所以就先初始化。给head和tail造一个空对象,第一次循环结束,
第二次循环,tail就不是nul,这个时候头和尾两个位置都不是null,就会进到else里面,因为是双线链表,就有前驱和后继节点 node的前驱是t,t的后继是node,,t指向的tail节点,node 接在t的后面,最后把tail节点替换为 node 节点。
最后把 t节点返回, 到这里,tail已经是我们传进来的节点。head还是之前的new出来的空节点。返回的节点是之前的 tail节点,这个tail节点连接了传递进来的node节点。

那现在继续回到addWaiter 方法里面,
如果tail不是null,还是和之前的一样,连接节点,把tail设置为当前构建的节点。最后把这个节点返回,返回的时候是 构建的节点和它的前驱节点.
最后的返回就是 当前构建的节点和它的前驱节点。

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)) {
                    //如果成功的话就将自己设置为头节点
                    setHead(node);
                    //释放头节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //失败之后 是不是应该park呢?
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //pack住
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

注意 这里是一个死循环,死循环肯定是要做成某件事的,要不然就会一直尝试。
先看node 的前驱是不是头节点,如果是头节点,那就说明自己还能在尝试一下,万一在尝试的时候,突然获得锁了,那多好。将头节点设置为自己,断掉前驱节点,让gc回收,如果不断掉(久而久之就会有内存的消耗,然后炸掉)
这种就说到 引用的类型上面了,之后再说。
如果上一步没有成功,就来到了 shouldParkAfterFailedAcquire 方面里面了,看这个名字就知道是个啥了, 是不是应该park住当获取锁失败之后呢? 看看这个方法里面是个啥
shouldParkAfterFailedAcquire()方法

    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;
    }

得到前驱节点的waitStatus,看是不是 是-1 ,是不是 >0 ,是不是=0

  • =0 表示这个节点是刚初始化好的,得把它为-1; 因为 waitStatus 为 -1 表示有资格唤醒它的后继节点
  • 0 就会找到 第一个 waitStatus为 -1 的节点,

  • = -1 表示这个这个节点已经可以安全的park住它的后继节点了
    返回为true就会到 parkAndCheckInterrupt 方法
    parkAndCheckInterrupt方法
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

在这里就可以pak住当前的线程了。注意这里用的 park和unpack。


到这里,一个lock就做完了 注意,每一个线程都会park在这里,之后如果唤醒之后就从这里继续走。

如果有线程释放了锁,就会unpark 链表中的线程。 线程就会继续从这里走,就还是从 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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

就会继续在这个for循环里面 尝试获取锁,获得到就运行,获取不到就会继续park.

看看释放锁的操作

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

调用还是同步器的 我这里默认的非公平的

 public final boolean release(int arg) {
        //尝试释放锁,主要就是将 state减-1  看是不是0 ,
        if (tryRelease(arg)) {
            
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

首先是 tryRelease 这个方法里面主要就是将 state-1 看是不是0,如果是0 就表示释放干净了,将当前锁的拥有者变为null,如果不是0,就说明有锁重入发生。
如果释放成功了就会进if块里面,接下来就唤醒链表中的节点了。
主要是在 unparkSuccessor 方法里面
unparkSuccessor

  private void unparkSuccessor(Node 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;
        //状态 < 0 
        if (ws < 0)
          //cas操作将-1 前一个节点的 state变为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;
//1 
        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;
        }
//2
        if (s != null)
            //调用unpark方法
            LockSupport.unpark(s.thread);
    }

需要注意的是 前一个节点的 state为 -1 的时候 才能唤醒下一个节点
这里的流程就是先找到头节点,看头节点的statue是否为-1,只有为-1 的时候时候才可以唤醒他的后继节点。
先不看 编号为 1 的注释, 先看 第2 个注释。
第2个还是 就是调用unpark方法,唤醒这个线程。这个线程唤醒之后从 acquireQueued 这个方法中继续走,因为在 acquireQueued 方法中是死循环,线程就从这里开始继续走,又再一次的获取锁。
能获取到就 运行,获取不到就park住
在看看编号为1 的注释。
在这个注释里,看他上面的英文,线程会unpark他的后继节点,正常情况下唤醒的是下一个节点,特殊情况下,就从 链表的尾开始找,找到第一个 找到第一个 statue为 -1 的节点,找到他之后unpark。
锁重入怎么解锁
如果是锁重入的话 每一次调用unlock方法的时候就回吧state -1 如果 state变为0 才会唤醒后面的线程,如果是不为0 的话,就会返回false

 protected final boolean tryRelease(int releases) {
             //在unlock里面 调用的时候每一次 releases 都是1    state - 1 
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //看 c是不是0 
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //设置 state ,这里不需要 cas操作,因为调用unlock方法的时候,肯定是持有锁的线程。
            setState(c);
            return free;
        }

大体基本上就是这个样子了
在简单的说一下
ReentrantLock锁的实现是 依赖于一个同步器 AbstractQueuedSynchronizer,ReentrantLock里面自己维护了两个同步器,一个是公平实现,一个是非公平实现
锁 就是 一个state ,谁拿到锁了,就把找个状态变为 1 ,没有拿到锁的就在 AbstractQueuedSynchronizer 里面的线程队列里面等着。将自己变为这把锁的持有者
有谁释放了锁(也就是将state变为0 了) 就在AbstractQueuedSynchronizer 里面的线程队列里面唤醒一个。

3.公平锁和非公平锁

在 ReentrantLock 中的实现是不通的 同步器,默认是非公平的
FairSync NonfairSync的实现
差别主要是在 释放锁之后抢锁的时候, 非公平的实现就是 如果一个线程刚刚释放完锁,这个时候来了一个新的线程,这个线程和线程队列里面的线程都会抢占锁
公平就是 新来的线程会别加在线程队列的后面。
打个断点 去看看
主要的方法有这个

  protected final boolean tryAcquire(int acquires) {
           
           final Thread current = Thread.currentThread();
           int c = getState();
           //看state
           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;
       }
   */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

在tryAcquire 失败之后就直接添加队列了,不会 非公平的一样,上来就 cas 尝试修改。

你可能感兴趣的:(Java并发,java,队列,多线程,并发编程,链表)