java的reentrantlock深入理解

本文详细解析了ReentrantLock如何利用AQS框架实现并发控制,涉及CAS操作、公平锁与非公平锁的区别、lock()和unlock()的实现流程,以及超时获取锁的机制。
摘要由CSDN通过智能技术生成

概要

主要用到AQS的并发工具类:

等待队列中node:

abstract static class Node {
        volatile Node prev;       // initially attached via casTail
        volatile Node next;       // visibly nonnull when signallable
        Thread waiter;            // 保存当前线程
        volatile int status;  
}

Lock方法大致流程:

首先尝试获得锁compareAndSetState(0, 1)(只有非公平锁才有)-》通过cas改变状态值和设置成当前线程-》获得锁失败后-》加入锁池-》首先通过头尾节点是否为null来判断是否需要创建链表-》首先创建一个空的头结点-》再讲当前节点加入进去-》若是链表已存在就加入链尾-》之后是在循环中阻塞-》直到成为头结点的下个节点而获得锁

公平是需要查看锁池链表中是否有节点,没有才去尝试获得锁,或是查看当前获得锁的是否是自己,若是,就进行重入

Unlock方法大致流程:

会调Release释放锁,-》当前状态为进行减1-》若是等于0就将当前线程置为null,并重新设置状态-》然后将锁池链表头结点后面正常的节点进行唤醒

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

CAS:Compare and Swap,比较并交换

CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

AbstractQueuedSynchronizer简称AQS

  1. 【ReentrantLock使用示例】
  2. private Lock lock = new ReentrantLock();
  3. public void test(){
  4.     lock.lock();
  5.     try{
  6.         doSomeThing();
  7.     }catch (Exception e){
  8.         // ignored
  9.     }finally {
  10.         lock.unlock();
  11.     }

}

AQS

是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。 

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

【lock()与unlock()实现原理】

锁种类

可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁

lock()流程

  1. ReentrantLock提供了两个构造器,分别是
  2. public ReentrantLock() {
  3.     sync = new NonfairSync();
  4. }
  5. public ReentrantLock(boolean fair) {
  6.     sync = fair ? new FairSync() : new NonfairSync();

}

默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。

  1. NonfairSync
  2. final void lock() {
  3.     if (compareAndSetState(0, 1))
  4.         setExclusiveOwnerThread(Thread.currentThread());
  5.     else
  6.         acquire(1);

}

通过CAS操作,修改state属性值获得锁

首先用一个CAS操作,判断state(ReentrantLock的属性)是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。

“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了

  1.     若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。
  2. public final void acquire(int arg) {
  3.     if (!tryAcquire(arg) &&
  4.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  5.         selfInterrupt();
  1. }
  2. 1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。
  3. tryAcquire(arg)
  4. final boolean nonfairTryAcquire(int acquires) {
  5.     //获取当前线程
  6.     final Thread current = Thread.currentThread();
  7.     //获取state变量值
  8.     int c = getState();
  9.     if (c == 0) { //没有线程占用锁
  10.         if (compareAndSetState(0, acquires)) {
  11.             //占用锁成功,设置独占线程为当前线程
  12.             setExclusiveOwnerThread(current);
  13.             return true;
  14.         }
  15.     } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
  16.         int nextc = c + acquires;
  17.         if (nextc < 0) // overflow
  18.             throw new Error("Maximum lock count exceeded");
  19.         // 更新state值为新的重入次数
  20.         setState(nextc);
  21.         return true;
  22.     }
  23.     //获取锁失败
  24.     return false;

}

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

若是抢占锁失败,进入等待队列中

2. 第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。

  1. 先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)/**
  2.  * 将新节点和当前线程关联并且入队列
  3.  * @param mode 独占/共享
  4.  * @return 新节点
  5.  */
  6. private Node addWaiter(Node mode) {
  7.     //初始化节点,设置关联线程和模式(独占 or 共享)
  8.     Node node = new Node(Thread.currentThread(), mode);
  9.     // 获取尾节点引用
  10.     Node pred = tail;
  11.     // 尾节点不为空,说明队列已经初始化过
  12.     if (pred != null) {
  13.         node.prev = pred;
  14.         // 设置新节点为尾节点
  15.         if (compareAndSetTail(pred, node)) {
  16.             pred.next = node;
  17.             return node;
  18.         }
  19.     }
  20.     // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
  21.     enq(node);
  22.     return node;
  1. }B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
        若队列为空时,初始化队列
  1. /**
  2.  * 初始化队列并且入队新节点
  3.  */
  4. private Node enq(final Node node) {
  5.     //开始自旋
  6.     for (;;) {
  7.         Node t = tail;
  8.         if (t == null) { // Must initialize
  9.             // 如果tail为空,则新建一个head节点,并且tail指向head
  10.             if (compareAndSetHead(new Node()))
  11.                 tail = head;
  12.         } else {
  13.             node.prev = t;
  14.             // tail不为空,将新节点入队
  15.             if (compareAndSetTail(t, node)) {
  16.                 t.next = node;
  17.                 return t;
  18.             }
  19.         }
  20.     }

}这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。

当B、C入等待队列后,此时AQS队列如下:

等待的线程挂起

        挂起前再次尝试获取锁
  1. 3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。/**
  2.  * 已经入队的线程尝试获取锁
  3.  */
  4. final boolean acquireQueued(final Node node, int arg) {
  5.     boolean failed = true; //标记是否成功获取锁
  6.     try {
  7.         boolean interrupted = false; //标记线程是否被中断过
  8.         for (;;) {
  9.             final Node p = node.predecessor(); //获取前驱节点
  10.             //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
  11.             if (p == head && tryAcquire(arg)) {
  12.                 setHead(node); // 获取成功,将当前节点设置为head节点
  13.                 p.next = null; // 原head节点出队,在某个时间点被GC回收
  14.                 failed = false; //获取成功
  15.                 return interrupted; //返回是否被中断过
  16.             }
  17.             // 判断获取失败后是否可以挂起,若可以则挂起
  18.             if (shouldParkAfterFailedAcquire(p, node) &&
  19.                     parkAndCheckInterrupt())
  20.                 // 线程若被中断,设置interrupted为true
  21.                 interrupted = true;
  22.         }
  23.     } finally {
  24.         if (failed)
  25.             cancelAcquire(node);
  26.     }
  1. }
真实挂起

code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。

        /**

  1.  * 判断当前线程获取锁失败之后是否需要挂起.
  2.  */
  3. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  4.     //前驱节点的状态
  5.     int ws = pred.waitStatus;
  6.     if (ws == Node.SIGNAL)
  7.         // 前驱节点状态为signal,返回true
  8.         return true;
  9.     // 前驱节点状态为CANCELLED
  10.     if (ws > 0) {
  11.         // 从队尾向前寻找第一个状态不为CANCELLED的节点,删除已取消的节点,已经取消的节点就不需要再争夺锁来运行了
  12.         do {
  13.             node.prev = pred = pred.prev;
  14.         } while (pred.waitStatus > 0);
  15.         pred.next = node;
  16.     } else {
  17.         // 将前驱节点的状态设置为SIGNAL
  18.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  19.     }
  20.     return false;
  21. }
  22.   
  23. /**
  24.  * 挂起当前线程,返回线程中断状态并重置
  25.  */
  26. private final boolean parkAndCheckInterrupt() {
  27.     LockSupport.park(this);
  28.     return Thread.interrupted();

}

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。

     整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

    最终队列可能会如下图所示

unlock()流程

  1. unlock()public void unlock() {
  2.     sync.release(1);
  3. }
  4.   
  5. public final boolean release(int arg) {
  6.     if (tryRelease(arg)) {
  7.         Node h = head;
  8.         if (h != null && h.waitStatus != 0)
  9.             unparkSuccessor(h);
  10.         return true;
  11.     }
  12.     return false;

}如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

  1.    最后我们再看下tryRelease的执行过程/**
  2.  * 释放当前线程占用的锁
  3.  * @param releases
  4.  * @return 是否释放成功
  5.  */
  6. protected final boolean tryRelease(int releases) {
  7.     // 计算释放后state值
  8.     int c = getState() - releases;
  9.     // 如果不是当前线程占用锁,那么抛出异常
  10.     if (Thread.currentThread() != getExclusiveOwnerThread())
  11.         throw new IllegalMonitorStateException();
  12.     boolean free = false;
  13.     if (c == 0) {
  14.         // 锁被重入次数为0,表示释放成功
  15.         free = true;
  16.         // 清空独占线程
  17.         setExclusiveOwnerThread(null);
  18.     }
  19.     // 更新state值
  20.     setState(c);
  21.     return free;

}这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。 

用一张流程图总结一下非公平锁的获取锁的过程。    

FairSync

公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,若是等待队列为空就尝试获取锁;若是队列中存在等待线程,直接执行acquireQueued(),

超时机制

  1.  在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。 public boolean tryLock(long timeout, TimeUnit unit)
  2.         throws InterruptedException {
  3.     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  1. }

还是调用了内部类里面的方法。我们继续向前

 public final boolean tryAcquireNanos(int arg, long nanosTimeout)

  1.         throws InterruptedException {
  2.     if (Thread.interrupted())
  3.         throw new InterruptedException();
  4.     return tryAcquire(arg) ||
  5.         doAcquireNanos(arg, nanosTimeout);
  1. }这里的语义是:如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。tryAcquire我们已经看过,这里重点看一下doAcquireNanos做了什么。/**
  2.  * 在有限的时间内去竞争锁
  3.  * @return 是否获取成功
  4.  */
  5. private boolean doAcquireNanos(int arg, long nanosTimeout)
  6.         throws InterruptedException {
  7.     // 起始时间
  8.     long lastTime = System.nanoTime();
  9.     // 线程入队
  10.     final Node node = addWaiter(Node.EXCLUSIVE);
  11.     boolean failed = true;
  12.     try {
  13.         // 又是自旋!
  14.         for (;;) {
  15.             // 获取前驱节点
  16.             final Node p = node.predecessor();
  17.             // 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
  18.             if (p == head && tryAcquire(arg)) {
  19.                 setHead(node);
  20.                 p.next = null; // help GC
  21.                 failed = false;
  22.                 return true;
  23.             }
  24.             // 如果已经超时,返回false
  25.             if (nanosTimeout <= 0)
  26.                 return false;
  27.             // 超时时间未到,且需要挂起
  28.             if (shouldParkAfterFailedAcquire(p, node) &&
  29.                     nanosTimeout > spinForTimeoutThreshold)
  30.                 // 阻塞当前线程直到超时时间到期
  31.                 LockSupport.parkNanos(this, nanosTimeout);
  32.             long now = System.nanoTime();
  33.             // 更新nanosTimeout
  34.             nanosTimeout -= now - lastTime;
  35.             lastTime = now;
  36.             if (Thread.interrupted())
  37.                 //相应中断
  38.                 throw new InterruptedException();
  39.         }
  40.     } finally {
  41.         if (failed)
  42.             cancelAcquire(node);
  43.     }

}doAcquireNanos的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值