ReentrantLock 源码解读之lock和unlock过程

mac2022-07-05  42

文中都是自己总结的,如果哪里逻辑不对或者写的不清楚的还请评论区中指出。

前言:本篇主要基于源码来解读ReetrantLock加锁和解锁的过程,reetrantLock的主体思想就是通过对锁status的加减操作来实现的,如果当前线程获得当前锁就把status+1,再次获取就继续+1,释放锁就是-1;    如果加锁的时候发现锁status不等于0就把当下线程放入到一个FIFO队列(就是一个双向链表)中并挂起,被放入等待队列的线程等待正在执行的线程唤醒它或者等待的线程被中断

当然了其中运用了线程自旋和cas以及volatile关键字来保证线程能够正确的获取锁和释放锁。


文中标注解的地方都是对理解整体流程没有影响的代码,这部分代码可以后续在看


一 .ReetrantLock 非公平锁讲解

1.非公平锁初始化,代码块1

//默认构造器,加载的为非公平锁 Public ReentrantLock(){ sync=new NonfairSync(); }

2.非公平锁加锁过程lock.lock()

非公平锁在获取锁时,直接去获取挣钱锁,而不管等待队列有没有等待的线程,这样尽可能的减少了加入等待队列及线程被挂起的时间,代码讲解如下,代码块2

// 获取锁 //status 需要说明下:status是一个锁的全局变量,加锁释放锁、或者重入锁(status多次加1)都是通过通过cas来原子的对status加1减1 来表示的。 final void lock() { //使用cas 尝试 把status 状态设置成1 设置成1 就表表获取锁了,要是失败说明已经被占用了 if (compareAndSetState(0, 1)) //注解2 //设置为当前线程占有锁 setExclusiveOwnerThread(Thread.currentThread());//注解3 //如果stateOffset 状态设置为1失败说明,有可能是当前线程再次获取锁,或者别的线程占有锁 else acquire(1); } //继承自aqs,再次尝试获取锁tryAcquire(重入锁 或者在这期间上个线程已经释放了锁) 如果获取了锁那么就返回true,获取锁失败的话就把该线程加入到等待队列 //addWaiter 用于把当前节点加入到等待队列中,通过cas 和自旋保证一定会加入成功 //acquireQueued:如果当前线程为首节点就自旋获取锁,否则挂起 ;直到被唤醒并获取锁,然后返回当前线程的中断位状态 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //acquireQueued该方法返回true说明,该线程是被中断唤醒 并获取的锁,所以需要重新设置下中断位 //selfInterrupt这里看不懂没关系,继续往下梳理加锁的主流程即可,回头再看 selfInterrupt();//调用 Thread.currentThread.interrupt() 把当然线程中断位 设成true, }

当我们调用lock.lock()获取非公平锁的时候,首先会先通过cas尝试获取锁,如果成功的抢占了锁,就省略了加入等待队列步骤。

如果获取锁失败的话,有可能持有锁的线程为当前线程,调用tryAcquire再次尝试获取锁。代码块3

//尝试获取锁, protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取当然锁的状态 int c = getState(); //如果锁状态为0说明在这期间上个线程已经释放锁 if (c == 0) { //通过cas 设置锁状态,如果设置成功就把锁设为当前线程独有 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果锁状态不等于0,则判断该锁是否为当前线程拥有(锁重入),如果是则把锁状态+1 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; //锁状态是个int类型,这里应该是担心锁重复的次数太多然后 溢出变成负数 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); //锁状态更新 setState(nextc); return true; } return false; }

再次尝试获取锁,首先判断status是否等于0,防止上个线程已经释放锁。如果没有释放锁,判断持有锁的线程是否为当前线程,如果持有锁的线程等于当前线程,那么当前锁为冲入锁,status再次+1

上述操作都失败的话,把当前线程加入到等待队列中。等待队列为一个FIFO队列,就是一个双向链表,ReetrantLock (继承来的)中存在了一个头结点指针 head 和一个尾节点指针tail(node节点的数据结构代码在注解1)

我们来看下线程加入锁等待队列的代码,如下:代码块4

//把当前线程加入到等待队列中去,等待队列为一个双向列表 private Node addWaiter(Node mode) { //给当前线程创建一个node节点,见注解1,mode传入的为null Node node = new Node(Thread.currentThread(), mode); //等待队列的尾节点指针 Node pred = tail; //如果尾节点不等于null 说明有别的线程在等待该锁 if (pred != null) { //把当前节点指向上一节点的指针指向尾节点原来指向的那个节点,就是把当前节点加入到尾节点 node.prev = pred; //通过cas把尾节点 指向为当前结点 //成功 ,则说明这期间没有别的线程加入到等待队列 //失败,则说明在这期间有别的线程已经加入到了尾节点 if (compareAndSetTail(pred, node)) { //上一个节点指向当前结点 pred.next = node; return node; } } enq(node); return node; }

这里乐观的认为可以直接加入到等待队列,如果发现并发或者该节点是第一次加入,调用enq方法初始化等待队列并通过cas及自旋保证成功加入到等待队列队尾。如下  代码块5

//第一种情况:第一次加入等待队列尾节点为null //第二种情况:第n次加入成功把当前线程节点加入到等待队列 //第三种情况:第n加入,加入到等待队列尾部的时候,有别的线程已经加入到等待队列了 private Node enq(final Node node) { //防止并发加入到等待队列的尾节点, for (;;) { Node t = tail; //尾节点为null,说明是第一次加入,需要初始化头尾节点 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方法来保证,当前线程节点一定成功的加入到等待队列,加入到等待队列之后呢?

我们来看下加入等待队列之后的acquireQueued操作:代码块6

//如果发现当前节点为首节点就自旋争抢锁,并返回当前线程的中断状态,用于后续操作判断当前线程的是怎么被唤醒的 //如果该节点还有前置节点就把当前线程挂起等待被唤醒 //被唤醒后,如果当前线程节点变为首节点就争抢锁,否则继续上两个步骤 //被唤醒且获取锁后,返回当前线程的中断状态(当且只有被挂起后才会可能返回true) final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //如果该节点是头节点的话 自旋争抢锁, for (;;) { //获取当前结点的前置节点 final Node p = node.predecessor(); //如果前置节点为head说明,他当前线程是等待队列中的第一个,那么就尝试获取锁 //获取锁成功就重置head节点,并返回false if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } //普通锁的情况下:shouldParkAfterFailedAcquire为第一次加入等待队列的节点设置节点等待状态(waitStatus =-1),然后返回false继续自旋 尝试获取锁 //shouldParkAfterFailedAcquire 只有发现当前节点不是首节点才会返回true ,然后挂起当前线程, if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //如果该线程是被中断唤醒的,用于辅助后续操作判断当前线程是被中断唤醒的 interrupted = true; } } finally { //如果该方法因为某些特殊情况意外的退出(没有获取锁就退出了),那么就取消尝试获取锁 //设置节点状态为CANCELLED if (failed) cancelAcquire(node); } } //只有 当该节点的前置节点为普通的锁时(没有使用condition)返回true //普通的锁第一次加入等待队列时把waitStatus 置为-1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; //如果当前节点不为首节点的话 直接返回true if (ws == Node.SIGNAL) return true; //pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。 //当前获取锁的时候发生某些不可知的错误就把,等待队列中的节点设成CANCELLED(1)状态 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //没有condition 的情况下,且是第一个加入到等待列表 //如果在这期间头结点没有边的话,就把当前节点waitStatus设置为-1状态, //node的状态为-1,说明当前节点的锁是一个普通的锁 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //挂起当前线程,并返回当前线程的中断状态(用于后续操作来判断该线程是怎么被唤醒的?), private final boolean parkAndCheckInterrupt() { //挂起当前线程 //park被唤醒的条件有 1.线程调用了unpark,2:其它线程中断了线程;3:发生了不可预料的事情 LockSupport.park(this); //返回当前线程的中断位状态,但是会清除中断位 return Thread.interrupted(); }

通过查看acquireQueued源码我们知道,当我们把当前线程节点加入到等待队列中后,如果该节点为首节点当前线程就自旋争抢锁,否则就调用LockSupport.park挂起当前线程,等待被调用uppark或者中断唤醒。只有被中断唤醒的并成功抢占锁的线程,acquireQueued方法才会返回true。

因为LockSupport.park挂起的线程不仅会被LockSupport.unpark方法唤醒,还会被中断唤醒,所以线程被唤醒后调用了下Thread.interrupted()方法来返回下当前线程的中断状态,但是该方法会清楚掉线程的中断状态,所以在代码块2又一次的设置了下中断状态。这也是ReentrantLock区别于synchronized关键字的一个地方。

ReentrantLock支持等待队列中的线程中断,synchronized不支持。

3.非公平锁释放锁的过程

lock.unlock(); public void unlock() { sync.release(1); } public final boolean release(int arg) { //tryRelease尝试释放锁(锁status-1),如果当前线程没有占有的锁(锁status=0) 返回true if (tryRelease(arg)) { //当前线程释放掉了所有锁(持有锁的线程) Node h = head; //如果等待队列第一个结点有挂起的线程,将它唤醒去争抢锁 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { //尝试释放一个锁 int c = getState() - releases; //判断释放锁的线程是否为当前持有锁的线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果当前锁状态为0,说明已经释放掉锁,并把当前锁持有线程设为null if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

3.lock.lockInterruptibly() 简述

这里不就贴源码了,过程和普通加锁很类似,唯一的区别在于lock.lockInterruptibly()方法遇到中断直接抛出中断异常,所在在使用该方法的时候需要处理下中断异常


二 .ReetrantLock 公平锁讲解

reentrantLock 支持公平锁和非公平锁,默认的构造方法初始化的是非公平锁,上文我们详细的介绍了非公平锁的源码,下面来看下公平锁怎么实现的

2.ReentrantLock lock = new ReentrantLock(true); //参数为true的时候调用的是 new FairSycn(); public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 来看看公平锁的lock方法: //ReentrantLock.FairSync.lock final void lock() { acquire(1); } //同非公平锁的方法,区别在于tryAcquire的实现(多态) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } //ReentrantLock.FairSync.tryAcquire //如果等待队列没有等待的节点或者,等待队列中的第一个节点为当前线程,尝试去获取锁 //区别于非公平锁的是:非公平锁加入等待队列之前不去判断有没有别的线程在等待锁,而直接尝试去争抢锁 //公平锁如果发现等待队列有别的线程在等待就不去争抢锁, 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; } //判断队列中有没有别的线程在等待当前锁,有返回true,没有返回false public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }

公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。

公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量

 


注解1:等待队列的节点数据结构

static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; //节点的等待状态 volatile int waitStatus; // 节点的前置节点指针 volatile Node prev; //节点的后置节点指针 volatile Node next; //等待的线程对象 volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }

注解2:cas操作

在加载ReentrantLock类的时候会加载如下代码

//我们现在只需要知道该类提供了cas操作 private static final Unsafe unsafe = Unsafe.getUnsafe(); //这些偏移量是相对于aqs对象的内存位置的偏移量 //线程的状态 private static final long stateOffset; //等待队列首节点的内存偏移量 private static final long headOffset; //等待队列尾节点的内存偏移量 private static final long tailOffset; private static final long waitStatusOffset; private static final long nextOffset; // 这块代码的作用是,aqs类加载的时候把相关字段的内存偏移量赋值给上边变量 //方便cas的一些操作,cas都是通过这些内存偏移量来设置上边的变量 static { try { stateOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("state")); headOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("head")); tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail")); waitStatusOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("waitStatus")); nextOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("next")); } catch (Exception ex) { throw new Error(ex); } }

我们在看来一下注解2的代码

//尝试通过 cas设置锁状态码 继承自aqs //stateOffset 是 aqs中 state 的内存地址(相对类的起始位置的偏移量) protected final boolean compareAndSetState(int expect, int update) { //如果stateOffset这个内存地址的值等于expect,就把该值设为update返回true,否则失败返回false return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

 


注解3:设置锁为当前线程独占

//设置为当前线程占有锁 继承自aqs,aqs继承自aos protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }

网上找了一张流程图

最新回复(0)