当前位置:首页 > 编程教程 > java技术文章 > Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)

Java AbstractQueuedSynchronizer源码分析

  • 发布时间:
  • 作者:码农之家
  • 点击:51

这篇文章主要知识点是关于Java、并发、AbstractQueuedSynchronizer、的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书

Web前端开发精品课 JavaScript基础教程
  • 类型:前端大小:9.7 MB格式:PDF作者:莫振杰
立即下载

Tags:Java并发 AbstractQueuedSynchronizer 

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个步骤。下面我们会逐个步骤进行演示分析。

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

第一步:!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,不同版本间会有差异,读者需要注意。

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

以上就是本次给大家分享的关于java的全部知识点内容总结,大家还可以在下方相关文章里找到相关文章进一步学习,感谢大家的阅读和支持。

您可能感兴趣的文章:

  • java解决分布式环境中高并发环境下数据插入重复问题
  • java 解决分布式环境中 高并发环境下数据插入重复问题 前言 原因:服务器同时接受到的重复请求 现象:数据重复插入 / 修改操作 解决方案 : 分布式锁 对请求报文生成 摘要信息 + redis 实现分布式锁 工具类 分布式锁的应用 package com.nursling.web.filter.context;import com.nursling.nosql.redis.RedisUtil;import com.nursling.sign.SignType;import com.nursling.sign.SignUtil;import redis.clients.jedis.Jedis;import javax.servlet.……

  • Java8 parallelStream并发安全原理讲解
  • 背景 Java8的stream接口极大地减少了for循环写法的复杂性,stream提供了map/reduce/collect等一系列聚合接口,还支持并发操作:parallelStream。 在爬虫开发过程中,经常会遇到遍历一个很大的集合做重复的操作,这时候如果使用串行执行会相当耗时,因此一般会采用多线程来提速。Java8的paralleStream用fork/join框架提供了并发执行能力。但是如果使用不当,很容易陷入误区。 Java8的paralleStre……

  • Java并发系列之CountDownLatch源码分析
  • CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都……

  • java并发问题知识点总结
  • 1什么是并发问题。 多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发问题。 银行两操作员同时操作同一账户就是典型的例子。比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户减去50元,A先提交,B后提交。最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050。这就是典型的并发问题。如何解决?可以用锁。……

  • JAVA并发工具常用设计套路示例代码
  • 前言 在学习JAVA并发工具时,分析JUC下的源码,发现有三个利器:状态、队列、CAS。 状态 一般是state属性,如AQS源码中的状态,是整个工具的核心,一般操作的执行都要看当前状态是什么, 由于状态是多线程共享的,所以都是volatile修饰,保证线程直接内存可见。 /*** AbstractQueuedSynchronizer中的状态*/private volatile int state;/*** Status field, taking on only the values:* SIGNAL: The successor of this no……

    Java并发 相关电子书
    学习笔记
    网友NO.267657

    浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一。 CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西。大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成。虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GHZ,甚至5GHZ的CPU比起来还是太慢了,在CPU的眼中,内存运行的速度简直就是弟弟中的弟弟,等内存进行一次读写操作,CPU能思考成百上千次人生了:grin:。但是CPU的运算能力是紧缺资源啊,可不能这么白白浪费了,所以得想办法解决这一个问题。 没有什么问题是一个缓存不……

    网友NO.872504

    Java并发编程包中atomic的实现原理示例详解

    线程安全: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的。 线程安全主要体现在以下三个方面: 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作 可见性:一个线程对主内存的修改可以及时的被其他线程观察到 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序 引子 在多线程的场景中,我们需要保证数据安全,就会考虑同步的方案,通常会使用synchronized或者lock来处理,使用了synchronized意味着内核态的一次切换。这是一个很重的操作。 有没有一种方式,可以比较便利的实现一些简单的数据同步,比如计数器等等。concurrent包下……

    网友NO.628264

    Java并发编程(CyclicBarrier)实例详解

    Java并发编程(CyclicBarrier)实例详解 前言: 使用JAVA编写并发程序的时候,我们需要仔细去思考一下并发流程的控制,如何让各个线程之间协作完成某项工作。有时候,我们启动N个线程去做一件事情,只有当这N个线程都达到某一个临界点的时候,我们才能继续下面的工作,就是说如果这N个线程中的某一个线程先到达预先定义好的临界点,它必须等待其他N-1线程也到达这个临界点,接下来的工作才能继续,只要这N个线程中有1个线程没有到达所谓的临界点,其他线程就算抢先到达了临界点,也只能等待,只有所有这N个线程都到达临界点后,接下来的事情才能继续。 一、场景描述 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到……

    <
    1
    >

    电子书 编程教程 PC软件下载 安卓软件下载

    Copyright 2018-2020 xz577.com 码农之家

    本站所有电子书资源不再提供下载地址,只分享来路

    免责声明:网站所有作品均由会员网上搜集共同更新,仅供读者预览及学习交流使用,下载后请24小时内删除

    版权投诉 / 书籍推广 / 赞助:QQ:520161757