Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)

在上一篇《Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析》中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态。理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解。在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作。AQS为在独占模式和共享模式下获取锁分别提供三种获取方式:不响应线程中断获取,响应线程中断获取,设置超时时间获取。这三种方式整体步骤大致是相同的,只有少部分不同的地方,所以理解了一种方式再看其他方式的实现都是大同小异。在本篇中我会着重讲不响应线程中断的获取方式,其他两种方式也会顺带讲一下不一致的地方。

1. 怎样以不响应线程中断获取锁?

//不响应中断方式获取(独占模式)
public final void acquire(int arg) {
  if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
    selfInterrupt();
  }
}

上面代码中虽然看起来简单,但是它按照顺序执行了下图所示的4个步骤。下面我们会逐个步骤进行演示分析。

第一步:!tryAcquire(arg)

 //尝试去获取锁(独占模式)
protected boolean tryAcquire(int arg) {
  throw new UnsupportedOperationException();
}

这时候来了一个人,他首先尝试着去敲了敲门,如果发现门没锁(tryAcquire(arg)=true),那就直接进去了。如果发现门锁了(tryAcquire(arg)=false),就执行下一步。这个tryAcquire方法决定了什么时候锁是开着的,什么时候锁是关闭的。这个方法必须要让子类去覆盖,重写里面的判断逻辑。

第二步:addWaiter(Node.EXCLUSIVE)

//将当前线程包装成结点并添加到同步队列尾部
private Node addWaiter(Node mode) {
  //指定持有锁的模式
  Node node = new Node(Thread.currentThread(), mode);
  //获取同步队列尾结点引用
  Node pred = tail;
  //如果尾结点不为空, 表明同步队列已存在结点
  if (pred != null) {
    //1.指向当前尾结点
    node.prev = pred;
    //2.设置当前结点为尾结点
    if (compareAndSetTail(pred, node)) {
      //3.将旧的尾结点的后继指向新的尾结点
      pred.next = node;
      return node;
    }
  }
  //否则表明同步队列还没有进行初始化
  enq(node);
  return node;
}

//结点入队操作
private Node enq(final Node node) {
  for (;;) {
    //获取同步队列尾结点引用
    Node t = tail;
    //如果尾结点为空说明同步队列还没有初始化
    if (t == null) {
      //初始化同步队列
      if (compareAndSetHead(new Node())) {
        tail = head;
      }
    } else {
      //1.指向当前尾结点
      node.prev = t;
      //2.设置当前结点为尾结点
      if (compareAndSetTail(t, node)) {
        //3.将旧的尾结点的后继指向新的尾结点
        t.next = node;
        return t;
      }
    }
  }
}

执行到这一步表明第一次获取锁失败,那么这个人就给自己领了块号码牌进入排队区去排队了,在领号码牌的时候会声明自己想要以什么样的方式来占用房间(独占模式or共享模式)。注意,这时候他并没有坐下来休息(将自己挂起)哦。

第三步:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

//以不可中断方式获取锁(独占模式)
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);
        //为了帮助垃圾收集, 将上一个head结点的后继清空
        p.next = null;
        //设置获取成功状态
        failed = false;
        //返回中断的状态, 整个循环执行到这里才是出口
        return interrupted;
      }
      //否则说明锁的状态还是不可获取, 这时判断是否可以挂起当前线程
      //如果判断结果为真则挂起当前线程, 否则继续循环, 在这期间线程不响应中断
      if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
        interrupted = true;
      }
    }
  } finally {
    //在最后确保如果获取失败就取消获取
    if (failed) {
      cancelAcquire(node);
    }
  }
}

//判断是否可以将当前结点挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  //获取前继结点的等待状态
  int ws = pred.waitStatus;
  //如果前继结点状态为SIGNAL, 表明前继结点会唤醒当前结点, 所以当前结点可以安心的挂起了
  if (ws == Node.SIGNAL) {
    return true;
  }

  if (ws > 0) {
    //下面的操作是清理同步队列中所有已取消的前继结点
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    //到这里表示前继结点状态不是SIGNAL, 很可能还是等于0, 这样的话前继结点就不会去唤醒当前结点了
    //所以当前结点必须要确保前继结点的状态为SIGNAL才能安心的挂起自己
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}

//挂起当前线程
private final boolean parkAndCheckInterrupt() {
  LockSupport.park(this);
  return Thread.interrupted();
}

领完号码牌进入排队区后就会立马执行这个方法,当一个结点首次进入排队区后有两种情况,一种是发现他前面的那个人已经离开座位进入房间了,那他就不坐下来休息了,会再次去敲一敲门看看那小子有没有完事。如果里面的人刚好完事出来了,都不用他叫自己就直接冲进去了。否则,就要考虑坐下来休息一会儿了,但是他还是不放心,如果他坐下来睡着后没人提醒他怎么办?他就在前面那人的座位上留一个小纸条,好让从里面出来的人看到纸条后能够唤醒他。还有一种情况是,当他进入排队区后发现前面还有好几个人在座位上排队呢,那他就可以安心的坐下来咪一会儿了,但在此之前他还是会在前面那人(此时已经睡着了)的座位上留一个纸条,好让这个人在走之前能够去唤醒自己。当一切事情办妥了之后,他就安安心心的睡觉了,注意,我们看到整个for循环就只有一个出口,那就是等线程成功的获取到锁之后才能出去,在没有获取到锁之前就一直是挂在for循环的parkAndCheckInterrupt()方法里头。线程被唤醒后也是从这个地方继续执行for循环。

第四步:selfInterrupt()

 //当前线程将自己中断
 private static void selfInterrupt() {
   Thread.currentThread().interrupt();
 }

由于上面整个线程一直是挂在for循环的parkAndCheckInterrupt()方法里头,没有成功获取到锁之前不响应任何形式的线程中断,只有当线程成功获取到锁并从for循环出来后,他才会查看在这期间是否有人要求中断线程,如果是的话再去调用selfInterrupt()方法将自己挂起。

2. 怎样以响应线程中断获取锁?

//以可中断模式获取锁(独占模式)
private void doAcquireInterruptibly(int arg) throws InterruptedException {
  //将当前线程包装成结点添加到同步队列中
  final Node node = addWaiter(Node.EXCLUSIVE);
  boolean failed = true;
  try {
    for (;;) {
      //获取当前结点的前继结点
      final Node p = node.predecessor();
      //如果p是head结点, 那么当前线程就再次尝试获取锁
      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);
    }
  }
}

响应线程中断方式和不响应线程中断方式获取锁流程上大致上是相同的。唯一的一点区别就是线程从parkAndCheckInterrupt方法中醒来后会检查线程是否中断,如果是的话就抛出InterruptedException异常,而不响应线程中断获取锁是在收到中断请求后只是设置一下中断状态,并不会立马结束当前获取锁的方法,一直到结点成功获取到锁之后才会根据中断状态决定是否将自己挂起。

3. 怎样设置超时时间获取锁?

//以限定超时时间获取锁(独占模式)
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
  //获取系统当前时间
  long lastTime = System.nanoTime();
  //将当前线程包装成结点添加到同步队列中
  final Node node = addWaiter(Node.EXCLUSIVE);
  boolean failed = true;
  try {
    for (;;) {
      //获取当前结点的前继结点
      final Node p = node.predecessor();
      //如果前继是head结点, 那么当前线程就再次尝试获取锁
      if (p == head && tryAcquire(arg)) {
        //更新head结点
        setHead(node);
        p.next = null;
        failed = false;
        return true;
      }
      //超时时间用完了就直接退出循环
      if (nanosTimeout <= 0) {
        return false;
      }
      //如果超时时间大于自旋时间, 那么等判断可以挂起线程之后就会将线程挂起一段时间
      if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) {
        //将当前线程挂起一段时间, 之后再自己醒来
        LockSupport.parkNanos(this, nanosTimeout);
      }
      //获取系统当前时间
      long now = System.nanoTime();
      //超时时间每次都减去获取锁的时间间隔
      nanosTimeout -= now - lastTime;
      //再次更新lastTime
      lastTime = now;
      //在获取锁的期间收到中断请求就抛出异常
      if (Thread.interrupted()) {
        throw new InterruptedException();
      }
    }
  } finally {
    if (failed) {
      cancelAcquire(node);
    }
  }
}

设置超时时间获取首先会去获取一下锁,第一次获取锁失败后会根据情况,如果传入的超时时间大于自旋时间那么就会将线程挂起一段时间,否则的话就会进行自旋,每次获取锁之后都会将超时时间减去获取一次锁所用的时间。一直到超时时间小于0也就说明超时时间用完了,那么这时就会结束获取锁的操作然后返回获取失败标志。注意在以超时时间获取锁的过程中是可以响应线程中断请求的。

4. 线程释放锁并离开同步队列是怎样进行的?

//释放锁的操作(独占模式)
public final boolean release(int arg) {
  //拨动密码锁, 看看是否能够开锁
  if (tryRelease(arg)) {
    //获取head结点
    Node h = head;
    //如果head结点不为空并且等待状态不等于0就去唤醒后继结点
    if (h != null && h.waitStatus != 0) {
      //唤醒后继结点
      unparkSuccessor(h);
    }
    return true;
  }
  return false;
}

//唤醒后继结点
private void unparkSuccessor(Node node) {
  //获取给定结点的等待状态
  int ws = node.waitStatus;
  //将等待状态更新为0
  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 <= 0) {
        s = t;
      }
    }
  }
  //唤醒给定结点后面首个不是取消状态的结点
  if (s != null) {
    LockSupport.unpark(s.thread);
  }
}

线程持有锁进入房间后就会去办自己的事情,等事情办完后它就会释放锁并离开房间。通过tryRelease方法可以拨动密码锁进行解锁,我们知道tryRelease方法是需要让子类去覆盖的,不同的子类实现的规则不一样,也就是说不同的子类设置的密码不一样。像在ReentrantLock当中,房间里面的人每调用tryRelease方法一次,state就减1,直到state减到0的时候密码锁就开了。大家想想这个过程像不像我们在不停的转动密码锁的转轮,而每次转动转轮数字只是减少1。CountDownLatch和这个也有点类似,只不过它不是一个人在转,而是多个人每人都去转一下,集中大家的力量把锁给开了。线程出了房间后它会找到自己原先的座位,也就是找到head结点。看看座位上有没有人给它留了小纸条,如果有的话它就知道有人睡着了需要让它帮忙唤醒,那么它就会去唤醒那个线程。如果没有的话就表明同步队列中暂时还没有人在等待,也没有人需要它唤醒,所以它就可以安心的离去了。以上过程就是在独占模式下释放锁的过程。

注:以上全部分析基于JDK1.7,不同版本间会有差异,读者需要注意。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java并发系列之CyclicBarrier源码分析

    现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始.例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始.在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类.利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作.下图演示了这一过程. 在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await

  • Java并发系列之AbstractQueuedSynchronizer源码分析(共享模式)

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快的理解其他的方式.虽然说AbstractQueuedSynchronizer源码有一千多行,但是重复的也比较多,所以读者不要刚开始的时候被吓到,只要耐着性子去看慢慢的自然能够渐渐领悟.就我个人经验来说,阅读AbstractQueuedSynchronizer源码有几个比较关键的地方需要弄明白,分别是

  • Java并发系列之CountDownLatch源码分析

    CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0.另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒.这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条

  • Java并发系列之AbstractQueuedSynchronizer源码分析(概要分析)

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

  • Java并发之ReentrantLock类源码解析

    ReentrantLock内部由Sync类实例实现. Sync类定义于ReentrantLock内部. Sync继承于AbstractQueuedSynchronizer. AbstractQueuedSynchronizer继承于AbstractOwnableSynchronizer. AbstractOwnableSynchronizer类中只定义了一个exclusiveOwnerThread变量,表示当前拥有的线程. 除了Sync类,ReentrantLock内部还定义了两个实现类. No

  • Java并发系列之Semaphore源码分析

    Semaphore(信号量)是JUC包中比较常用到的一个类,它是AQS共享模式的一个应用,可以允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制.Semaphore提供了一个许可证的概念,可以把这个许可证看作公共汽车车票,只有成功获取车票的人才能够上车,并且车票是有一定数量的,不可能毫无限制的发下去,这样就会导致公交车超载.所以当车票发完的时候(公交车以满载),其他人就只能等下一趟车了.如果中途有人下车,那么他的位置将会空闲出来,因此如果这时其他人想要上车

  • Java并发系列之ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

  • Java并发系列之AbstractQueuedSynchronizer源码分析(条件队列)

    通过前面三篇的分析,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列和条件队列.我们还是拿公共厕所做比喻,同步队列是主要的排队区,如果公共厕所没开放,所有想要进入厕所的人都得在这里排队.而条件队列主要是为条件等待设置的,我们想象一下如果一个人通过排队终于成功获取锁进入了厕所,但在方便之前发现自己没带手纸,碰到这种情况虽然很无奈,但是它

  • Java并发系列之ConcurrentHashMap源码分析

    我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

  • Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

  • Java并发编程之Condition源码分析(推荐)

    Condition介绍 上篇文章讲了ReentrantLock的加锁和释放锁的使用,这篇文章是对ReentrantLock的补充.ReentrantLock#newCondition()可以创建Condition,在ReentrantLock加锁过程中可以利用Condition阻塞当前线程并临时释放锁,待另外线程获取到锁并在逻辑后通知阻塞线程"激活".Condition常用在基于异步通信的同步机制实现中,比如dubbo中的请求和获取应答结果的实现. 常用方法 Condition中主要的

随机推荐