JUC源码学习笔记1——AQS和ReentrantLock

打印 上一主题 下一主题

主题 672|帖子 672|积分 2016

  1. 笔记主要参考《Java并发编程的艺术》并且基于JDK1.8的源码进行的刨析,此篇只分析独占模式,后续在ReentrantReadWriteLock和 CountDownLatch中 会重点分析AQS的共享模式
复制代码
一丶Lock

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁可以防止多个线程同时访问共享资源(这种锁称为独占锁,排他锁)但是有些锁可以允许多个线程并发访问共享资源,比如读写锁
1.Lock接口的方法:

方法作用void lock()获取锁,调用该方法的线程将会获取锁,当锁获得之后从该方法返回void lockInterruptibly()可中断地获取锁,该方法会响应中断,在锁的获取可以中断当前线程,如果在获取锁之前设置了中断标志,or获取锁的中途被中断or其他线程中断该线程则抛出InterruptedException并清除当前线程的中断状boolean tryLock()尝试非阻塞的获取锁,调用方法会立即返回,如果可以获取到锁返回trueboolean tryLock(long time, TimeUnit unit) throws InterruptedException超时获取锁,从当前返回有三种情况
1.超时时间内获取到锁
2.当前线程在超时时间内被中断3.超时间结束没有获取到锁,返回falsevoid unLock释放锁Condition newCondition()获取等待通知的组件,该组件和当前锁绑定,只有获取到锁调用wait方法后当前线程将放弃锁,后续被其他线程signal继续争抢锁2.Lock相比synchronized具备的特性


  • 尝试非阻塞的获取锁
  • 响应中断的获取锁
  • 超时获取锁
  1. synchronized相比于Lock 更加简单,更不容易犯错,但是不够灵活
复制代码
3.使用Lock的经典范式


获取锁的过程不要写在try中,避免获取锁失败最后finally释放其他线程持有的锁

二丶AbstractQueuedSynchronizer队列同步器

使用一个int成员变量state表示同步状态,内置的FIFO队列来完成资源的获取和线程的排队工作,支持独占也支持共享的获取同步状态。

三个变量被volatile修饰,保证其线程可见性
1.队列同步器可以被重写的方法

方法说明protected boolean tryAcquire(int arg)独占的获取锁,需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS改变同步状态protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态protected int tryAcquireShared(int arg)共享式的获取同步状态,放回大于等于0()的值表示成功,反之失败protected boolean tryReleaseShared(int arg)共享式释放同步状态protected boolean isHeldExclusively()当前队列同步器释放再独占模式下被线程占用,一般表示当前线程是否独占2.队列同步器提供的模板方法

方法说明void acquire(int arg)独占式获取同步状态,如果获取成功那么直接返回,反之进入同步队列中等待,void acquireInterruptibly(int arg)和acquire,但是此方法支持在获取锁的过程中响应中断,如果当前线程被中断那么抛出InterruptedExceptionboolean tryAcquireNanos(int arg, long nanosTimeout)在acquireInterruptibly的基础上增加了超时限制,如果在指定时间内没有获取到同步状态那么返回false反之truevoid acquireShared(int arg)共享式获取同步状态,如果没有获取到那么进入等待队列等待,和acquire不同的式支持同一个时刻多个线程获取同步状态void acquireSharedInterruptibly(int arg)和acquireShared类似但是支持响应中断boolean tryAcquireSharedNanos(int arg, long nanosTimeout)在acquireSharedInterruptibly新增了超时限制boolean release(int arg)独占式释放同步资源,在释放同步状态后唤醒后继线程boolean releaseShared(int arg)共享式释放同步状态Collection getQueuedThreads()获取等待在同步队列上的线程们3.同步队列的节点属性

属性描述int waitStatus等待状态Node pre前驱节点Node next后继节点Node nextWaiter等待队列中的后继节点,如果当前节点式共享模式,那么这个节点是SHARED常量,也就是说节点类型和等待中后继节点是公用一个字段Thread thread获取同步状态的线程等待状态是一个枚举,具备下列可选的值

  • CANCELLED(1)线程获取锁超时or被中断,需要从同步队列中取消中断,节点进入改状态后状态不再改变
  • SIGNAL(-1)后继节点线程处于等待状态,而当前节点的线程如果释放共享资源或者被取消会通知后继节点,使后继线程被唤醒继续执行
  • CONDITION(-2)节点在等待队列中,节点中的线程在Condition上进行等待,需要等待其他线程调用Condition的singal or singalAll进行唤醒,该节点会从等待队列移动到同步队列,进行共享资源的争夺
  • PROPAGATE(-3)表示下一次共享式同步状态的获取将无条件的传播下去
  • 0 初始状态,节点假如到同步队列时候的状态
4.AQS怎么维护同步队列

AQS中包含两个节点类型引用:头节点和尾节点。当一个线程获取到同步状态的时候,其他线程无法获取,将被放入到同步队列中,加入队列这个过程为了保证线程安全而采用CAS。同步队列遵守FIFO,头节点是获取到同步状态的线程,释放同步状态将会唤醒后继线程,后继节点获取到同步状态后将被设置为头节点
三丶ReentrantLock可重入锁

1.ReentrantLock简介

支持公平和非公平和重入的独占式锁

  • 重入表示已经获得锁的线程可以对共享资源重复加锁
  • 公平锁,支持先来后到,像在公司排队上厕所,先来的人肯定优先获取到茅坑,先来的线程肯定先获取到共享资源
  • 独占式,同一时间只允许一个线程操作共享资源
2.公平锁和非公平锁比较

公平锁在头节点释放同步资源的时候需要unpark后续节点,并切换线程执行上下文,导致效率并不如非公平锁,但是公平锁可以减少饥饿,因为非公平锁好像A在排队,A获取到共享资源需要进行唤醒和上下文切换,而导致需要更多时间,这时候流氓B刚好进厕所门,上来就是一个CAS,很快抢占了厕所这一共享资源,导致A处于饥饿——迟迟得不到厕所(共享资源)的操作资源。
3.ReentrantLock的可重入

实现可重入需要解决两个问题

  • 线程再次获得锁,锁需要去识别当前线程释放是当前占据锁的线程,如果是那么直接加锁成功
  • 锁的最终释放,加锁多少次,就需要释放多少次,完全解锁后其他线程才可以获取到锁。
第一个问题ReentrantLock通过获取当前线程和独占锁线程的`==1判断来实现,第二个问题ReentrantLock通过对AQS中的共享资源state增加和减少来实现
四丶结合ReentrantLock分析加锁解锁的流程

1.ReentranLock


ReentrantLock的公平和非公平就是由于sync引用指向了的不同实现,其lock unlock等操作也是一律交由到sync
2.ReentrantLock的非公平模式

2.1非公平加锁——lock方法

加锁的大致流程



  • 无论是非公平还是公平在加锁成功后都会通过setExclusiveOwnerThread设置当前线程为独占锁的线程,这个方法会记住当前线程,这是后面实现可重入的关键、
  • acquire 方法会调用tryAcquire方法,这个方法由AQS的子类实现,NonfairSync这里会调用nonfairTryAcquire方法
2.1.1不公平的尝试获取共享资源nonfairTryAcquire



  • 如果nonfairTryAcquire返回true表示当前线程获取到了锁,那么皆大欢喜,当前线程可以继续运行
  • 返回false的情况

    • 共享资源是0,但是同一个时间多个线程抢占,当前这个线程CAS失败了
    • 共享资源不是0,当前线程也不是独占的线程
    这两种情况都需要继续执行AQS的acquire方法

2.1.2AQS的acquire 方法

独占模式获取共享资源,对中断不敏感,或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出
1.执行流程


2.将当前线程包装成Node加入到队列尾addWaiter



  • 快速入队
    下面这段代码值得品一品
    1. Node pred = tail;
    2. if (pred != null) {
    3.         //当前线程的前置设置为尾,这一步那么多个线程执行这一步也是无关紧要的
    4.     //只是把当前节点的前置改变了,不是改变pred的next指向
    5.     node.prev = pred;
    6.     //CAS设置尾节点 为当前节点,这个自选操作compareAndSetTail是线程安全,同一时间只有一个线程可以设置自己为尾节点
    7.     if (compareAndSetTail(pred, node)) {
    8.         //注意 如果原尾节点是S,线程A设置成功 那么尾巴被修改为了A,假如A执行下面一行的时候消耗完了时间片,线程B进来了,这时候线程B拿到的tail就是A,所以不会存在线程安全问题
    9.         pred.next = node;
    10.         return node;
    11.     }
    12. }
    复制代码
  • 完整入队
    !
    完整入队和快速入队差不多,就是多了一个初始化的逻辑
    那么为什么不直接完整入队,也许是for循环比if多更多的字节码需要执行?也许Doug Lea测试多次后发现快速入队后完整入队,比直接完整入队效率更高
3.尝试出队acquireQueued



  • 如何从自旋中退出
    前继节点是头节点,头节点是当前获取到共享资源的节点,且获取共享资源tryAcquire成功
  • 挂起当前线程避免无休止的自选
    自选是cpu操作,无限制的自选是很浪费cpu资源的

如果shouldParkAfterFailedAcquire放回true 表示当前线程需要被挂起,会继续执行parkAndCheckInterrupt,这个方法很简单只有两行
  1. private final boolean parkAndCheckInterrupt() {
  2.         //挂起当前线程
  3.     LockSupport.park(this);
  4.    
  5.     //返回中断状态,并且清除中断标识
  6.     return Thread.interrupted();
  7. }
复制代码
如果parkAndCheckInterrupt 返回了true 表示当前线程被中断过,并且会让外层的acquireQueued返回true,会导致acquire执行当前线程的自我中断


理解这一段代码需要对java中断机制具备一定理解
java线程中断机制


  • 调用Thread的interrupt方法

    • 如果线程处于Running状态那么只是修改Thread内部的中断标识值为true
    • 如果线程由于sleep,wait,join等方法进入等待状态,会直接抛出中断异常并清楚中断标识
    • 如果线程由于LockSupport.park进入等待状态,调用该线程的interrupt方法只会让LockSupport.park返回

  • interrupt,interrupted,isInterrupted三个方法比较

    • interrupt 见上⬆
    • interrupted 返回当前线程的中断标识并且充值中断标识
    • isInterrupted返回中断标识

我们继续说为什么当前线程在获取锁的途中被中断,需要自我中断以下
  1. acquire的"需求":
  2. 独占模式获取共享资源,对中断不敏感,或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出
复制代码
线程获取同步状态的时候被中断会发生什么——从LockSupport.park(this)中返回继续拿锁,这就是为什么说acquire的对中断不敏感。
  1. LockSupport.park();不会抛出受检查异常,当出现被打断的情况下,线程被唤醒后,我们可以通过Interrupt的状态来判断,我们的线程是不是被interrupt的还是被unpark或者到达指定休眠时间
复制代码
假如我们写如下这样的代码执行

存在一个调度线程中断了上面的线程,但是上面的线程还在抢夺锁,并且被park了,这时候上面线程的park会返回,并且清除中断标识,如果不进行自我中断,那么下面while内容还是会进行,那么我们调度线程的中断就无效了
3.ReentrantLock的公平模式

  1. 传入true获取一个公平锁
复制代码
3.1公平的获取锁——lock方法


公平锁的lock方法直接调用AQS的acquire方法,上面我们分析的acquire方法它会先去调用tryAcquire,这个tryAcquire被FairSync重写

  • FairSync的tryAcquire方法
  1. protected final boolean tryAcquire(int acquires) {
  2.         final Thread current = Thread.currentThread();
  3.         int c = getState();
  4.      //共享状态当前空闲
  5.         if (c == 0) {
  6.             //前面没有节点 这就是公平是怎么实现的
  7.             //且cas成功 那么拿到锁
  8.             if (!hasQueuedPredecessors() &&
  9.                 compareAndSetState(0, acquires)) {
  10.                 setExclusiveOwnerThread(current);
  11.                 return true;
  12.             }
  13.         }
  14.      //实现重入 和 公平锁一样
  15.         else if (current == getExclusiveOwnerThread()) {
  16.             int nextc = c + acquires;
  17.             if (nextc < 0)
  18.                 throw new Error("Maximum lock count exceeded");
  19.             setState(nextc);
  20.             return true;
  21.         }
  22.         return false;
  23.     }
  24. }
复制代码
源码没什么很难的点,就是通过判断前面时候还有节点(标识是否由线程比当前线程先到)如果没有那么再去拿锁,如果共享状态不是0且当前线程不是独占的线程那么就会执行acquireQueued方法,在acquireQueued里面自选获取锁会判断前一个节点是否是头节点且调用tryAcquire
4.释放锁

释放锁直接调用AQS的release方法,其中tryRelease方法由ReentrantLock中Sync自己实现(公平or非公平都一样)
  1. public final boolean release(int arg) {
  2.     //完全的释放资源
  3.     if (tryRelease(arg)) {
  4.         Node h = head;
  5.         //头节点初始化的时候才为0,但是后面如果由节点加入到同步队列会把前置节点的状态设置为Singnal
  6.         if (h != null && h.waitStatus != 0)
  7.             //唤醒后继节点
  8.             unparkSuccessor(h);
  9.         return true;
  10.     }
  11.     return false;
  12. }
复制代码
4.1 tryRelease
  1. protected final boolean tryRelease(int releases) {
  2.         //重入了n次,当前释放m次 c=n-m
  3.     int c = getState() - releases;
  4.     //如果不是独占锁的线程 那么抛出异常
  5.     if (Thread.currentThread() != getExclusiveOwnerThread())
  6.         throw new IllegalMonitorStateException();
  7.         //是否完全的释放了锁
  8.     boolean free = false;
  9.         //只有剩下的为0 才是完全释放锁
  10.     if (c == 0) {
  11.         //置为true
  12.         free = true;
  13.                 //独占线程设置为null
  14.         setExclusiveOwnerThread(null);
  15.     }
  16.     //修改state
  17.     setState(c);
  18.     return free;
  19. }
复制代码
需要注意的是只有完全的释放了共享资源,在ReentrantLock里就是加锁n次解锁n次,才返回true,才会去唤醒后继节点
4.2 unparkSuccessor

[code]private void unparkSuccessor(Node node) {    int ws = node.waitStatus;    if (ws < 0)        compareAndSetWaitStatus(node, ws, 0);    Node s = node.next;    if (s == null || s.waitStatus > 0) {        s = null;        //从尾巴开始找到队列最前面的且需要通知的节点 为什么要从尾巴开始找?        for (Node t = tail; t != null && t != node; t = t.prev)            if (t.waitStatus  spinForTimeoutThreshold,剩余时间大于阈值(1000)才会挂起,如果小于的化还是进行自旋,因为非常短的超时时间无法做到十分精确(挂起和唤醒也是需要时间的)如果还是进行超时等待反而会表现得不精确</p>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

三尺非寒

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表