打通 Java 任督二脉 —— 并发数据结构的基石

简介: 每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLock,使用它加锁时如果没有立即加成功,就会阻塞当前的线程等待其它...

每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLock,使用它加锁时如果没有立即加成功,就会阻塞当前的线程等待其它线程释放锁之后再重新尝试加锁,那线程是如何实现阻塞自己的?其它线程释放锁之后又是如果唤醒当前线程的?当前线程是如何得出自己没有加锁成功这一结论的?本篇内容将会从根源上回答上面提到的所有问题

线程阻塞原语

Java 的线程阻塞和唤醒是通过 Unsafe 类的 park 和 unpark 方法做到的。

 

这两个方法都是 native 方法,它们本身是由 C 语言来实现的核心功能。park 的意思是停车,让当前运行的线程 Thread.currentThread() 休眠,unpark 的意思是解除停车,唤醒指定线程。这两个方法在底层是使用操作系统提供的信号量机制来实现的。具体实现过程要深究 C 代码,这里暂时不去具体分析。park 方法的两个参数用来控制休眠多长时间,第一个参数 isAbsolute 表示第二个参数是绝对时间还是相对时间,单位是毫秒。

线程从启动开始就会一直跑,除了操作系统的任务调度策略外,它只有在调用 park 的时候才会暂停运行。锁可以暂停线程的奥秘所在正是因为锁在底层调用了 park 方法。

parkBlocker

线程对象 Thread 里面有一个重要的属性 parkBlocker,它保存当前线程因为什么而 park。就好比停车场上停了很多车,这些车主都是来参加一场拍卖会的,等拍下自己想要的物品后,就把车开走。那么这里的 parkBlocker 大约就是指这场「拍卖会」。它是一系列冲突线程的管理者协调者,哪个线程该休眠该唤醒都是由它来控制的。

 

 

当线程被 unpark 唤醒后,这个属性会被置为 null。Unsafe.park 和 unpark 并不会帮我们设置 parkBlocker 属性,负责管理这个属性的工具类是 LockSupport,它对 Unsafe 这两个方法进行了简单的包装。

 

Java 的锁数据结构正是通过调用 LockSupport 来实现休眠与唤醒的。线程对象里面的 parkBlocker 字段的值就是下面我们要讲的「排队管理器」。

排队管理器

当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。每一把锁内部都会有这样一个队列管理器,管理器里面会维护一个等待的线程队列。ReentrantLock 里面的队列管理器是 AbstractQueuedSynchronizer,它内部的等待队列是一个双向列表结构,列表中的每个节点的结构如下。

 

加锁不成功时,当前的线程就会把自己纳入到等待链表的尾部,然后调用 LockSupport.park 将自己休眠。其它线程解锁时,会从链表的表头取一个节点,调用 LockSupport.unpark 唤醒它。

 

AbstractQueuedSynchronizer 类是一个抽象类,它是所有的锁队列管理器的父类,JDK 中的各种形式的锁其内部的队列管理器都继承了这个类,它是 Java 并发世界的核心基石。比如 ReentrantLock、ReadWriteLock、CountDownLatch、Semaphone、ThreadPoolExecutor 内部的队列管理器都是它的子类。这个抽象类暴露了一些抽象方法,每一种锁都需要对这个管理器进行定制。而 JDK 内置的所有并发数据结构都是在这些锁的保护下完成的,它是JDK 多线程高楼大厦的地基。

 

锁管理器维护的只是一个普通的双向列表形式的队列,这个数据结构很简单,但是仔细维护起来却相当复杂,因为它需要精细考虑多线程并发问题,每一行代码都写的无比小心。

JDK 锁管理器的实现者是 Douglas S. Lea,Java 并发包几乎全是他单枪匹马写出来的,在算法的世界里越是精巧的东西越是适合一个人来做。

Douglas S. Lea是纽约州立大学奥斯威戈分校计算机科学教授和现任计算机科学系主任,专门研究并发编程和并发数据结构的设计。他是Java Community Process的执行委员会成员,主持JSR 166,它为Java编程语言添加了并发实用程序。

 

后面我们将 AbstractQueuedSynchronizer 简写成 AQS。我必须提醒各位读者,AQS 太复杂了,如果在理解它的路上遇到了挫折,这很正常。目前市场上并不存在一本可以轻松理解 AQS 的书籍,能够吃透 AQS 的人太少太少,我自己也不算。

公平锁与非公平锁

公平锁会确保请求锁和获得锁的顺序,如果在某个点锁正处于自由状态,这时有一个线程要尝试加锁,公平锁还必须查看当前有没有其它线程排在排队,而非公平锁可以直接插队。联想一下在肯德基买汉堡时的排队场景。

也许你会问,如果某个锁处于自由状态,那它怎么会有排队的线程呢?我们假设此刻持有锁的线程刚刚释放了锁,它唤醒了等待队列中第一个节点线程,这时候被唤醒的线程刚刚从 park 方法返回,接下来它就会尝试去加锁,那么从 park 返回到加锁之间的状态就是锁的自由态,这很短暂,而这短暂的时间内还可能有其它线程也在尝试加锁。

其次还有一点需要注意,执行了 Lock.park 方法的线程自我休眠后,并不是非要等到其它线程 unpark 了自己才会醒来,它可能随时会以某种未知的原因醒来。我们看源码注释,park 返回的原因有四种

1.其它线程 unpark 了当前线程

2.时间到了自然醒(park 有时间参数)

3.其它线程 interrupt 了当前线程

4.其它未知原因导致的「假醒」

文档中没有明确说明何种未知原因会导致假醒,它倒是说明了当 park 方法返回时并不意味着锁自由了,醒过来的线程在重新尝试获取锁失败后将会再次 park 自己。所以加锁的过程需要写在一个循环里,在成功拿到锁之前可能会进行多次尝试。

计算机世界非公平锁的服务效率要高于公平锁,所以 Java 默认的锁都使用了非公平锁。不过现实世界似乎非公平锁的效率会差一点,比如在肯德基如果可以不停插队,你可以想象现场肯定一片混乱。为什么计算机世界和现实世界会有差异,大概是因为在计算机世界里某个线程插队并不会导致其它线程抱怨。

 


共享锁与排他锁

ReentrantLock 的锁是排他锁,一个线程持有,其它线程都必须等待。而 ReadWriteLock 里面的读锁不是排他锁,它允许多线程同时持有读锁,这是共享锁。共享锁和排他锁是通过 Node 类里面的 nextWaiter 字段区分的。

那为什么这个字段没有命名成 mode 或者 type 或者干脆直接叫 shared?这是因为 nextWaiter 在其它场景还有不一样的用途,它就像 C 语言联合类型的字段一样随机应变,只不过 Java 语言没有联合类型。


欢迎工作一到五年的Java工程师朋友们加入Java填坑之路:860113481
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
 

相关文章
|
5月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
67 1
|
5月前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
117 2
|
5月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
128 2
|
3月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
66 5
|
4月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
104 5
|
4月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
4月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
74 6
|
4月前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
4月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
4月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
66 2

热门文章

最新文章