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

简介: Java技术指北

大家好,我是指北君。

最近在学习Java并发编程,但学了很久,总觉得差点意思,因为只会使用相关工具类,却不知实现原理,有时候写出了bug也不知道啥原因,所以指北君一怒之下,决定死磕java.util .concurrent工具包下的源码!!经过一个月的熬灯夜读,指北君总于小有所成,现在决定输出这一个月的所有收获!

JUC包下的类这么多,我们先从哪个开始呢?这个答案是唯一的,那就是AQS!

前言

别急,在指北君谈AQS之前,想先问问大家知不知道并发需要关注的两个核心问题?不知道?那这个必须知道!第一个问题是互斥,即同一时间只允许一个线程访问共享资源,第二个是同步,即线程如何进行通信和协作。

那我们又是怎么解决这两个问题的呢?那就是我们大名鼎鼎的管程和信号量了!但我们今天先说说管程,为啥大家后面就知道了。

那什么是管程呢?所谓管程,就是管理共享变量以及对共享变量操作的过程,其有三种模型,分别为 Hasen 模型、Hoare 模型和 MESA 模型。目前应用最广泛的是MESA模型,而JAVA采用的也是这种MESA模型(其模型图如下图所示):

20.png

可能这个图大家现在还看不太明白,没关系,暂时留个印象,当看完指北君AQS系列文章以后,你再回过头来看这个图,肯定秒懂!

Java中的synchronized关键字就是其管程的具体实现,当然,今天所要聊的AQS同样也是。AQS是Java并发编程的基础,只要掌握它,java.util .concurrent工具包下的大部分工具类源码你都能在10分钟内看懂,连源码都懂了,还怕不知道怎么用吗?!

所以,针对AQS,指北君将用三篇文章来讲解:

第一篇即本篇,我会将AQS做个整体的介绍,并将其基础总结成了三把斧头,有了这三把斧头,你就能快速斩获AQS

第二篇我们则讲AQS如何通过锁机制解决互斥问题

第三篇则是AQS如何通过条件变量来解决线程通信协作问题

好了,现在随着指北君开始第一篇的学习吧


AQS是什么

好了,本文的正篇正式开始。前言说了半天AQS,AQS到底是什么呢?AQS全称为AbstractQueuedSynchronizer,翻译成中文即:抽象队列同步器,它是java.util .concurrent工具包下的抽象类,也是一个模板类(设计模式中的模板模式可以了解一波),你也可以理解为为开发人员提供的一种同步框架,它已经帮我们实现了大部分公共通用逻辑,如线程入队、出队,阻塞、唤醒等,我们只需要根据我们自己的需求,实现一些特定的方法,这些方法也叫钩子方法,比如下面几个方法:

  1. tryAcquire:独占模式下获取同步状态
  2. tryRelease:独占模式下释放同步状态
  3. tryAcquireShared:共享模式下获取同步状态
  4. tryReleaseShared:共享模式下释放同步状态
  5. isHeldExclusively:独占模式下,查看当前线程是否获取同步状态

AQS针对互斥,提供了两种模式,即独占模式和共享模式。独占模式只允许一个线程拿到锁去操作共享资源,而共享锁则有多把锁,允许多个线程同时操作共享资源,这个第二篇会详细讲解,在这之前我们需要先了解AQS的三板斧,这三个是了解AQS的基础:

  1. 状态:AQS中的所有逻辑都是依据状态state来进行的,所以它是整个类的和兴。它是被关键字volatile修饰的,保证其可见性和部分有序性
  2. 队列:一共有两种队列,同步队列和条件队列。当线程的请求在短时间内得不到满足时,线程会被包装成某种类型的数据结构放入队列中,当条件满足时则会拿出队列。
  3. CAS:由Unsafe工具类来实现的,其操作具有原子性,AQS通过CAS和volatile来保证状态的线程安全


第一板斧:状态

21.png

状态是被volatile修饰的int类型变量,它的值代表着锁还剩多少。在独占锁模式下,只有一把锁,则state只有0和1两个值,1代表锁没被其他线程占有,目前可获取;0则代表锁已经被其他线程占有了。在共享锁模式下,state最大值就是锁的数量。

后面我们对state的修改都是通过CAS操作,所以是线程安全的。


第二板斧:队列

AQS中存在两种队列,一种叫同步队列,它是双向链表,其获取锁失败的线程会被包装成Node放入同步队列中;另一种叫条件队列,它是单向链表,线程可以调用等待方法来把自己放入条件队列,或者调用唤醒方法把条件队列的其他被包装成Node的线程移到同步队列中。这个大家如果此时看不太懂没关系,第三篇文章会详细介绍这个等待唤醒机制。我们来看看线程包装成Node的Node类:

22.png23.png

指北君已经在源码上做了详细的注释,但这几个关键的属性上还是提出来单独看看。

下面四个属性是和节点相关的:

  1. prev:双向链表的前驱节点
  2. next:双向链表的后继节点
  3. thread:节点所代表的线程
  4. waitStatus:该节点线程所处的状态,即等待锁的状态

下面四个属性是waitStatus的具体状态:

  1. CANCELLED:此节点的线程被取消了
  2. SIGNAL:此节点的后继节点线程被挂起,需要被唤醒
  3. CONDITION:此节点的线程在等待信号,也表明当前节点不在同步队列中,而在条件队列中
  4. PROPAGATE:此节点下一个acquireShared应该无条件传播

关于waitStatus有几个点需要注意下:

  1. waitStatus除了上面四个状态,还有一个隐式的状态为0,即在Node初始化的时候
  2. 在独占锁模式下,只会有到状态CANCELLED和SIGNAL。需要特别注意的是,SIGNAL它代表的不是自己线程的状态,而是它后继节点的状态,当一个节点waitStatus为SIGNAL时,意味着此节点的后继节点被挂起,当此节点释放锁或者被取消拿锁,应该要唤醒后继节点
  3. 在共享锁模式下,只会用到状态CANCELLED和PROPAGATE


第三板斧:CAS

CAS又叫比较交换操作,它是Unsafe类中compareAndSwapXXX方法,我通过下面的例子做个简单的介绍:

24.png

CAS执行时,会拿地址tailOffset上的值与expect做比较,如果相同,则会将地址上的值更新为update,并返回true,否则直接返回false。

了解了CAS的基本例子后,我们看下AQS中CAS相关的代码:

25.png26.png

我们说第二板斧的时候说过,其五个属性state、head、tail,waitStatus、next都是被volatile修饰的,所以CAS对其操作能保证其线程安全,因此也可以猜到这些属性肯定是多线程争着修改的目标。静态块里则是对这五个属性偏移量进行初始化。



总结

AQS的三板斧就介绍完啦,我们再来简单回顾下:

AQS(AbstractQueuedSynchronizer)是java.util .concurrent工具包下的抽象类,它通过实现MESA管程来解决并发领域中的同步与互斥问题。AQS实现中最重要的三点就是状态、CAS和队列,我们也称之为AQS的三板斧。AQS的一切操作都是依据状态state来的,它是被volatile修饰的全局变量,因此我们通过CAS操作使其线程安全。队列是维护阻塞等待线程的容器,所有未获得锁或被要求等待的线程都会被包装成Node放入队列中。

AQS第一篇就到这里了,这篇是AQS的基础,而后面指北君会通过另外两篇文章具体介绍AQS是如何解决并发领域中的互斥和同步问题的,敬请期待~

相关文章
|
27天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
27 0
|
29天前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
9天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
13天前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
47 12
|
9天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
85 2
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
2月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
26天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
26天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
50 3
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
145 6