听说你想学Java并发编程?先把这个学了(2)

简介: Java技术指北

大家好,我是指北君。

俗话说,铁要趁热打,指北君在写完AQS第一篇文章后,就马不停蹄的输出第二篇了,这篇主要是讲AQS是如何解决互斥问题的,如果没看过AQS系列第一篇的童鞋,建议先把第一篇看完,它是后面两篇的基础。

说到互斥,我们第一个反应是什么?锁!对,AQS就是利用的锁来解决互斥的,那我们就来看看AQS是如何实现这个锁的。

AQS提供了两种锁,独占锁和共享锁。独占锁只有一把锁,同一时间只允许一个线程获得锁;而共享锁则有多把锁,同一时间允许多个线程获得锁。我们本文主要讲独占锁。

一. 独占锁的获取

AQS中对独占锁的获取一共有三个方法:

  1. acquire:不响应中断获取独占锁
  2. acquireInterruptibly:响应中断获取独占锁
  3. tryAcquireNanos:响应中断+超时获取独占锁

由于篇幅,我们主要着眼于acquire方法,当然,只要你理解了acquire,acquireInterruptibly和tryAcquireNanos自然不在话下了,因为这两个方法只是在acquire的基础上增加了一些判断逻辑来处理中断和超时情况而已。

我们上源码1.png

其acquire方法中一共有四个方法,其逻辑也分为4步:

  1. tryAcquire:尝试获取锁,成功即acquire方法结束,否则调用addWaiter
  2. addWaiter:获取锁失败即调用此方法入队,即将获取锁失败的线程包装成Node放入同步队列的队尾
  3. acquireQueued:入队成功后即调用此方法,如果Node在队首则再次抢锁,否则挂起等待唤醒(唤醒后再去获取锁)
  4. selfInterrupt:如果是被中断唤醒,则再次执行中断

粗略介绍完后,我们现在一个一个方法看。


1.1 tryAcquire

2.png

tryAcquire是钩子方法,是我们根据需要重写的。其功能就是在独占模式下去获取锁,获取成功则返回true,acquire方法直接结束;如果获取失败返回false,则后续会调用后面要讲的addWaiter方法将线程入队。

因为AQS是模板类,不同的子类只需要重写不同的钩子方法,因此,tryAcquire不能设置成抽象方法,不然一些不需要此钩子方法的子类也要实现这个方法。所以作者对tryAcquire的默认实现是抛了一个异常(当然我认为直接写个return也是ok的)。


1.2  addWaiter

如果tryAcquire获取锁失败后,我们就会调用addWaiter将线程包装成Node入队挂起。addWaiter的大致逻辑是:先将线程包装成Node,然后入队,如果队列未初始化或者入队失败,则会调用子方法enq,enq来进行初始化队列和自旋入队,我们看下具体代码:

3.png下面是enq方法,当执行到这个方法时,说明线程获取锁已经失败了,然后入队过程又失败了,入队过程失败有两个原因:

  1. 同步队列未初始化
  2. 入队过程中CAS操作失败


4.png

CAS节点入队失败的原因,我们看到enq源码中执行完尾插法的步骤一,即将Node的前驱指针指向当前尾结点,如果是并发情况下,应该是如下图所示(紫色节点代表我们关注的Node):


5.jpg

此时,可能有多个Node都准备入队,所以此时可能有多个Node的前驱节点都指向尾结点,所以我们在执行步骤二将尾结点指向Node时,采用的是CAS,即只有一个Node能成功,假设我们关注的Node入队成功了,如下图:

6.jpg

则另外两个CAS操作肯定会失败,即它们将要进入enq方法重新自旋入队。

1.3  acquireQueued

执行完addWaiter方法后,说明我们已经入队成功了,此时我们需要将Node中的线程挂起,等待下次被唤醒。

但在挂起之前,我们需要再次检查下我们此时的Node是否是在队首,如果在队首,我们又会再次去抢锁。否则我们会通过shouldParkAfterFailedAcquire判断是否要挂起(shouldParkAfterFailedAcquire不仅仅是判断此线程是否可以被挂起,还会将同步队列中属性为CANCELLED的Node移除队列),如果需要挂起,则调用parkAndCheckInterrupt将线程挂起。具体源码如下:

7.png8.png

shouldParkAfterFailedAcquire源码如下。其主要作用有2:

  1. 决定获取锁失败后,是否将线程挂起
  2. 清除同步队列中所有状态为CANCELLED的节点

9.png

这是acquireQueued中的最后一步,即将线程挂起,然后静静的等待被唤醒。除非该线程被其他线程unpark或者被中断,否则该线程的程序将一直停止在这。

10.png

1.4 selfInterrupt

通过我们前面的分析可以知道,当线程被中断过,则会进入到此方法。

而interrupte这个方法也只是将当前线程的中断标志置为true,至于会不会被中断,这个是由系统决定的。

image.png


二. 独占锁的的释放

相比独占锁的获取,独占锁的释放逻辑就简单多了。独占锁释放只做了两件事情:

  1. 释放锁
  2. 唤醒head结点后最近需要被唤醒的节点。

其释放逻辑的实现是通过release方法,而做的两件事分别对应了其子方法tryRelease和unparkSuccessor:


image.png


2.1 tryRelease

这个方法和tryAcquire一样,也是钩子方法,是留给子类重写的,作用是用来释放锁,如果释放成功则返回true,失败返回false,这个具体的实现我们也放在后续AQS的子类中讲解,这里就不过多阐述了。


2.2 unparkSuccessor

此方法的作用是唤醒后继Node,我们看代码:



image.png


这里需要注意的是,我们在找需要被唤醒的节点时,为什么是从后往前遍历呢?

其实这和获取锁时的尾结点入队有关,我们再看下入队方法addWaiter中插入尾结点的相关代码:

image.png

假设我们此时有个Node正在入队,执行完step2,还未执行step3,unparkSuccessor中如果采用从head往后遍历,是找不到这个新插入的Node的;但如果是采用从后往前遍历,则不会出现这个问题。


三. 总结

对于独占锁的获取与释放,指北君就分析完了,这里我再总结一下:

获取独占锁是通过acquire来实现的,首先通过tryAcquire获取锁,如果获取成功,则直接返回,如果失败,则会调用addWaiter方法进行入队,如果入队过程中发现队列未初始化,则会初始化队列再进行入队,入队不成功则会一直自旋直到成功;入队成功后就会挂起,直到被其他线程或者中断唤醒;唤醒后会检查线程的中断标志位,如果被中断过,会再次调用中断方法,告诉系统自己需要被中断。

释放独占锁是通过release方法实现的,其首先通过tryRelease释放锁,如果失败则直接返回false,如果成功则会调用unparkSuccessor唤醒后继节点。

通过上面的分析,大家应该了解了AQS是如何解决互斥问题的。后面指北君将会讨论AQS如何解决线程间通信协作问题,敬请期待~

相关文章
|
2天前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
|
5天前
|
Java API Apache
Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
【10月更文挑战第29天】Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
27 5
|
3天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
4天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
19 4
|
4天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
24 3
|
2天前
|
设计模式 安全 Java
Java编程中的单例模式深入解析
【10月更文挑战第31天】在编程世界中,设计模式就像是建筑中的蓝图,它们定义了解决常见问题的最佳实践。本文将通过浅显易懂的语言带你深入了解Java中广泛应用的单例模式,并展示如何实现它。
|
4天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
16 2
|
5天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
17 1
|
9天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
17 3
|
11天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。