Android 开发中的并发编程:从关键字到锁机制 在 Kotlin/Android 开发中,处理多线程并发主要有两个核心目标:可见性(Visibility) 和 原子性(Atomicity) 。为了达成这些目标,Java 语言及其并发包(JUC)提供了从简单到复杂的多种工具。
1. 基础关键字:原子性与可见性的保障 volatile
volatile 是最轻量级的同步机制。
作用: 保证变量的可见性 和禁止指令重排序 。
原理: 当一个线程修改了 volatile 变量,新值会立即同步到主内存;其他线程读取时,会强制从主内存读取。
局限: 不保证原子性 。例如 i++ 操作在多线程下依然是不安全的。
典型案例:为什么 DCL 单例必须加 Volatile? 以下是你代码中的问题分析及修正方案:
❌ 错误代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Singleton { private static volatile Singleton instance = null ; private static final Object lock = new Object (); private Singleton () {} public static Singleton getInstance () { if (instance == null ) { synchronized (lock) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
在 Java 5 之后,volatile 语义得到了增强。它不仅保证了线程 A 对 instance 的修改对线程 B 可见,更重要的是它在 new Singleton() 时建立了一个内存屏障。 如果没有 volatile,instance = new Singleton(); 可能会被分解为:
allocate (分配内存)
instance = memory (设置引用,此时对象还没初始化)
ctorSingleton (执行构造函数)
如果 2 和 3 发生重排序,其他线程就会拿到一个“半成品”对象。
在单例模式的 DCL (Double Check Locking) 中,volatile 的核心作用其实是禁止指令重排序。 如果没有 volatile,对象可能在构造函数还没执行完时,就把内存地址赋给了变量。另一个线程拿到的就是一个“半成品”对象,直接崩溃。
用途: 确保“在变量赋值之前,之前所有的初始化工作已经全部完成”。 场景: 刚才提到的单例模式赋值,或者作为“完成信号”标记。
再深入理解一下 作为“触发器 / 内存栅栏” (Memory Barrier) 当线程 A 写入一个 volatile 变量时,JMM (Java 内存模型) 会把该线程之前所有的普通变量修改也一起刷新到主内存中。
1 2 3 4 5 6 7 8 9 10 11 12 13 var data = 0 @Volatile var ready = false data = 42 ready = true ------------------------- 这里好似有一层屏障------------------------------ if (ready) { println(data ) }
所以 volatile 仅用于变量,且有能力影响其他的变量
2. 现代并发原语:CAS (Compare And Swap) 为什么不叫“修改”而叫“交换”? 这源于底层 CPU 指令(如 x86 的 LOCK CMPXCHG)。在硬件层面,这个操作通常是原子的,它会将寄存器里的新值与内存位置的值进行交换(或者说覆盖),以此确保在多核环境下,这一系列动作不会被其他处理器中途干扰。
CAS 是实现无锁编程(Lock-Free)的核心算法。
原理: 它包含三个操作数:内存地址 V、旧的预期值 A、即将更新的新值 B。只有当内存值 V 等于 A 时,才会将 V 修改为 B。
优点: 避免了线程切换的开销,效率极高。
缺点: 1. ABA 问题 (可用版本号解决)。
自旋时间长会消耗 CPU。
应用: Kotlin 中的 AtomicInteger, AtomicBoolean, AtomicReference 等都是基于 CAS 实现的。
特性
AtomicInteger
AtomicReference
操作目标
具体的 Int 数值
对象的引用地址
原子范围
数值的加减、替换
整个对象的切换/替换
性能
极高(直接操作底层数字)
高(操作指针),但对象内部属性不保证原子性
Kotlin 示例
AtomicInteger(0)
AtomicReference(User("Gemini"))
CAS 完整日志流 (含冲突重试) 第一轮:遭遇冲突
[读取] 内存值 V = 10。
[准备] 预期值 A = 10,拟更新值 B = 11。
[验证] 比较 V 是否为 A?
[冲突] 硬件返回 No (此时变量已被其他线程改成了 12)。
[动作] 写入失败,触发**自旋 (Retry)**。
第二轮:重试命中
[读取] 重新获取内存值 V = 12。
[准备] 预期值 A = 12,拟更新值 B = 13。
[验证] 比较 V 是否为 A?
[命中] 硬件返回 Yes (值未被改动)。
[结束] 成功写入 13,更新完成。
3. 基础锁机制:Synchronized synchronized 是 Java 中最常用的同步机制,它是一种隐式锁 (由 JVM 自动管理获取和释放),也是一种互斥锁 。
核心用法
修饰实例方法 :锁住当前实例对象 (this)。
修饰静态方法 :锁住当前类的 Class 对象。
修饰代码块 :锁住指定的对象实例(最灵活,建议缩小锁的范围)。
底层原理:Monitor(管程) 每个 Java 对象天生就带了一把“锁”,在 JVM 中通过 Monitor 机制实现。
**Enter (入锁)**:执行 monitorenter 指令,线程尝试获取对象的 Monitor。
**Exit (释放)**:执行 monitorexit 指令,线程释放 Monitor(即使发生异常,JVM 也会保证释放)。
JVM 对它的“黑科技”优化 在早期 Java 版本中,synchronized 是笨重的重量级锁。但从 Java 6 开始,引入了锁升级 路径以提升性能:
**偏向锁 (Biased Locking)**:锁会偏向于第一个访问它的线程,无竞争时几乎无开销。
**轻量级锁 (Lightweight Locking)**:当出现少量竞争时,通过 CAS (自旋)来获取锁,避免线程挂起。
**重量级锁 (Heavyweight Locking)**:当竞争激烈时,升级为重量级锁,线程进入阻塞状态,交给操作系统调度。
4. 高级锁机制:ReentrantLock 家族 当 synchronized 的灵活性不足时,我们会使用 java.util.concurrent.locks 中的工具。
ReentrantLock (可重入锁)
对比 synchronized: * 手动控制: 必须手动调用 lock() 和 unlock()(建议配合 try-finally)。
公平性: 支持公平锁(按等待顺序获取)。
可中断: 支持 lockInterruptibly()。
超时: 支持 tryLock(timeout),避免死锁。
ReentrantReadWriteLock (读写锁)
核心逻辑: 读读共享,读写互斥,写写互斥 。
场景: 适用于“读多写少”的情况。
特点: 允许多个线程同时读取资源,但只有一个线程能进行写入。如果已有线程在读,写线程必须等待。
synchronized 和 ReentrantLock 的区别 编写一个程序,开启三个线程(分别为 Thread A, Thread B, Thread C):
Thread A 负责打印字母 A。
Thread B 负责打印字母 B。
Thread C 负责打印字母 C。
要求:
这三个线程必须顺序执行 ,即控制台输出的结果必须是 ABCABCABC... 的形式。
每个线程各自循环打印 $n$ 次(例如你代码中的 loops = 5)。
1. 使用 synchronized 这是最基础的实现方式,依靠 wait() 让出锁,并依靠 notifyAll() 唤醒其他正在等待该锁的线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import kotlin.concurrent.threadfun synchronizedExample () { val lock = Object() var state = 0 val loops = 5 val t1 = thread { repeat(loops) { synchronized(lock) { while (state != 0 ) lock.wait() print("A" ) state = 1 lock.notifyAll() } } } val t2 = thread { repeat(loops) { synchronized(lock) { while (state != 1 ) lock.wait() print("B" ) state = 2 lock.notifyAll() } } } val t3 = thread { repeat(loops) { synchronized(lock) { while (state != 2 ) lock.wait() print("C " ) state = 0 lock.notifyAll() } } } }
2. 使用 ReentrantLock ReentrantLock 配合 Condition 的好处是它可以精准唤醒 。在上面的 synchronized 例子中,notifyAll() 会唤醒所有线程,但其实只有下一个顺序的线程才是真正需要的。使用 Condition 可以直接定向唤醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import java.util.concurrent.locks.ReentrantLockimport kotlin.concurrent.threadfun reentrantLockExample () { val lock = ReentrantLock() val conditionA = lock.newCondition() val conditionB = lock.newCondition() val conditionC = lock.newCondition() var state = 0 val loops = 5 thread(name = "A" ) { repeat(loops) { lock.lock() try { if (state != 0 ) conditionA.await() print("A" ) state = 1 conditionB.signal() } finally { lock.unlock() } } } thread(name = "B" ) { repeat(loops) { lock.lock() try { if (state != 1 ) conditionB.await() print("B" ) state = 2 conditionC.signal() } finally { lock.unlock() } } } thread(name = "C" ) { repeat(loops) { lock.lock() try { if (state != 2 ) conditionC.await() print("C " ) state = 0 conditionA.signal() } finally { lock.unlock() } } } }
3. 关键区别点
特性
synchronized
ReentrantLock
灵活性
自动加锁/释放,代码简洁
需手动 lock() 和 unlock(),更安全需配合 finally
唤醒机制
notifyAll() 唤醒所有,由线程竞争
Condition 可以实现精准“点名”唤醒,性能更优
等待响应
不可中断
支持 lockInterruptibly(),可中断等待
4. 终极性能优化:StampedLock StampedLock 是 Java 8 引入的,是对读写锁的改进。
核心改进: 引入了乐观读(Optimistic Reading) 。
原理: 读操作时不会阻塞写操作。读完后检查一个“戳(Stamp)”,如果期间没有写操作发生,则读取成功;如果有写操作,再退化为悲观读锁。
注意: 它不可重入,且不支持 Condition。
5. Kotlin 线程安全的数据结构 深入分类解读
数据结构
底层原理
特点
最佳场景
ConcurrentHashMap
CAS + synchronized (分段锁定)
高并发读写,允许完全并发的读
缓存、全局状态管理
CopyOnWriteArrayList
写时复制副本 (Copy-On-Write)
读操作无锁,写操作极慢且耗内存
监听器列表、配置读取 (读多写极少)
CopyOnWriteArraySet
基于 CopyOnWriteArrayList
去重,且具备写时复制特性
白名单、去重的回调通知
ConcurrentLinkedQueue
CAS (无锁算法)
非阻塞、高性能、无界
高频率、高并发的任务调度
LinkedBlockingQueue
独占锁 (ReentrantLock)
阻塞式,可选有界/无界
线程池任务队列 (ExecutorService)
ArrayBlockingQueue
独占锁 + 数组
阻塞式,必须指定容量
生产者-消费者(防止内存溢出)
1. “快读” 阵营:写时复制 (Copy-On-Write)
成员 :CopyOnWriteArrayList, CopyOnWriteArraySet
原理 :当你修改集合时,它不直接在原数组上改,而是复制一个新数组,改完后再把引用指向新的。
优点 :遍历(Iterator)时不需要加锁,绝对不会抛出 ConcurrentModificationException。
缺点 :写操作代价极高(内存占用翻倍)。
2. “高频” 阵营:无锁/细粒度锁 (CAS/Segment)
成员 :ConcurrentHashMap, ConcurrentLinkedQueue
原理 :使用硬件层面的 CAS (Compare And Swap) 原子操作。ConcurrentHashMap 在 Java 8+ 之后对桶位(Bucket)加锁,而不是整个 Map。
优点 :吞吐量极大,多个线程可以同时往里塞数据而不需要互相排队。
3. “同步” 阵营:阻塞队列 (Blocking)
成员 :LinkedBlockingQueue, ArrayBlockingQueue
原理 :利用 Condition 实现“等待-唤醒”机制。如果队列满了,写线程会停下等;如果队列空了,读线程会停下等。
区别 :
Array 是预先分配好内存的,适合对内存控制严格的场景。
Linked 是动态分配的,吞吐量通常略高于 Array,但要注意如果不设置容量限制,可能会导致 OOM。
1. ConcurrentHashMap 原理 (Java 8+) ConcurrentHashMap 的核心目标是在保证线程安全的同时,提供接近 HashMap 的高性能。
核心架构:由“分段锁”进化为“桶位锁” 在 Java 8 之后,它摒弃了 Java 7 的 Segment 方案,改用 CAS + synchronized + 红黑树 的混合结构。
A. 关键组件
Node 数组 :存储数据的核心数组。
**CAS (Compare And Swap)**:用于无锁地插入新节点或更新状态。
synchronized :仅锁定当前正在操作的 桶(Bucket)的头节点 。
红黑树 :当链表长度超过 8 时,转化为红黑树以保证查询效率为 O(\log n)。
我们可以从以下几个维度来深度理解这个设计:
1. 为什么要锁“头节点”? 在散列表中,数组的每一个下标(Bucket)都是一个入口。如果你要往某个链表里增加节点,或者修改某个节点,你必须保证在这个过程中没有其他人也在动这根链表。
头节点就是“守门员” :头节点是访问整个桶位的唯一入口。通过 synchronized(f)(这里的 f 就是头节点对象),Java 实际上是把这个桶位变成了一个同步块 。
最小化粒度 :只要两个线程访问的是不同的数组下标(比如线程 A 在下标 1,线程 B 在下标 5),它们之间完全没有锁竞争。这也就是为什么它的并发性能极高。
2. 为什么不用 CAS 到底,非要用 synchronized? 这是一个很棒的工程权衡。你会发现:
当桶位为空时 :使用 CAS 。因为这时候只需要把新节点挂上去,不涉及复杂逻辑,CAS 失败了重试即可。
当桶位有数据时 :情况变复杂了。你可能需要遍历链表看 Key 是否重复,或者需要旋转红黑树。
如果用 CAS 实现复杂的链表插入,逻辑会变得异常复杂(需要处理各种中间态)。
synchronized 的进化 :在 Java 8 中,synchronized 已经过优化(偏向锁、轻量级锁、锁消除等),在只有少量线程竞争时,性能非常出色。
代码可读性与稳定性 :使用 synchronized 锁定头节点,可以让后续的链表/红黑树操作变成“单线程化”,确保了逻辑的简单和安全。
B. 读写流程
写操作(put) :
计算 Hash 值,定位到数组下标。
如果目标位置为空,利用 CAS 尝试插入。
如果目标位置已有数据且正在扩容,则协助扩容。
如果目标位置有数据且未扩容,使用 synchronized 锁定该位置的 头节点 ,然后进行链表或红黑树的插入。
读操作(get) :
完全无锁 。Node 节点的 value 和 next 指针使用了 volatile 关键字,确保了多线程间的可见性。
C. 扩容机制 (Multiphase Transfer) 它是并发扩容的。当一个线程发现需要扩容时,它会参与进来协助数据的迁移,每个线程负责处理一小部分桶位,通过 ForwardingNode 标记已处理过的区域。
ConcurrentLinkedQueue 原理 ConcurrentLinkedQueue 是 Java 并发包(java.util.concurrent)中性能最高的队列之一。它是一个基于链接节点的无界线程安全队列 ,采用了 Michael & Scott 非阻塞队列算法 。
其核心思想是:不使用锁(synchronized 或 ReentrantLock),而是完全通过 CAS(Compare-And-Swap)原子操作来实现。
1. 核心架构与设计 ConcurrentLinkedQueue 内部维护了两个关键指针:head(头节点)和 tail(尾节点)。每个节点(Node)包含数据 item 和指向下一个节点的 next 指针。
原子性保障 :item 和 next 都被声明为 volatile,确保了多线程间的内存可见性。
哨兵节点 :初始化时,队列会创建一个空的“伪节点”,head 和 tail 最初都指向它。这简化了边界条件判断。
2. 关键算法:延迟更新(HOPS 策略) 这是 ConcurrentLinkedQueue 最精妙的地方。在传统的无锁算法中,每次插入都要更新一次 tail,但 CAS 是昂贵的(涉及缓存一致性流量)。为了提高吞吐量,该队列并不保证 tail 永远指向最后一个节点。
A. 入队 (offer) 的逻辑
寻找真正的尾部 :由于 tail 可能是“旧的”,程序会从 tail 开始往后遍历 next,直到找到真正的最后一个节点(即 next == null 的节点)。
CAS 插入 :使用 CAS 将真正的尾节点的 next 指向新节点。
延迟更新 tail :只有当 tail 偏离真正末尾的距离超过一个阈值(通常是 1 个节点长度)时,才会通过 CAS 将 tail 指向新节点。
直观理解 :就像接力赛,并不是每一棒都要把旗子插在终点,而是跑几棒后再统一更新终点标志的位置。
B. 出队 (poll) 的逻辑
寻找真正的头部 :head 也不一定指向第一个有效元素。它可能指向一个已经被弹出的节点。
CAS 提取 :找到第一个 item 不为 null 的节点,尝试用 CAS 将其 item 置为 null。
延迟更新 head :同理,只有当 head 偏离位置较远时,才会更新 head 指针。
3. CAS 如何解决竞争? 假设两个线程同时尝试入队:
线程 A 执行 CAS 成功,将新节点挂载到了队列末尾。
线程 B 的 CAS 会失败(因为预期的 null 变成了线程 A 插入的新节点)。
自旋重试 :线程 B 发现失败后,不会挂起,而是进入下一轮循环,重新定位新的尾节点并再次尝试 CAS。
优点
无锁化 :避免了线程切换和阻塞的开销。
高性能 :在高并发场景(尤其是生产者和消费者都很频繁时)下,吞吐量远超 LinkedBlockingQueue。
永不失效的迭代器 :它的迭代器是弱一致性的,不会抛出 ConcurrentModificationException。
缺点/局限
size() 开销大 :由于是链表结构且没有全局计数器(维护计数器本身需要同步),计算 size 必须遍历全表,复杂度为 $O(n)$。
内存压力 :无界队列如果不加控制,在生产者远快于消费者时可能会导致 OOM。
ConcurrentLinkedQueue 是通过 CAS 原子指令 + 延迟指针更新 实现的。它通过牺牲一定的实时准确性(如 tail 指针的滞后)换取了极高的并发处理能力,是实现高性能异步处理任务的理想选择。
以下是触发 tail 偏离并重新校准的具体场景分析:
1. 典型的“跳跃更新”场景(Hops = 2) 为了平衡 CAS 的性能开销,Doug Lea(JUC 作者)设计了让 tail 滞后。
初始状态 :head 和 tail 都指向同一个哨兵节点(Node 0)。
第一次插入(Thread A) :
发现 tail.next == null。
CAS 将 tail.next 指向新节点 Node 1。
不更新 tail。此时 tail 依然指向 Node 0,但 Node 0 指向 Node 1。这就产生了 偏离 (距离为 1)。
第二次插入(Thread B 或 A) :
检查 tail.next,发现不为 null(指向了 Node 1)。
顺着指针找到真正的末尾 Node 1。
CAS 将 Node 1 的 next 指向 Node 2。
触发阈值 :因为此时 tail 距离真正的末尾已经跳了 2 步,代码会执行 compareAndSetTail,将 tail 直接指向 Node 2。
2. 多线程竞争导致的“大幅偏离” 在极端高并发下,tail 偏离的距离可能远超 2 个节点:
竞速状态 :当线程 A 正在寻找末尾准备插入时,线程 B、C、D 已经快速完成了三次插入,但它们还没来得及更新 tail 指针。
被迫寻根 :此时线程 A 发现 tail.next 后面跟着一串节点(Node 1 -> Node 2 -> Node 3)。线程 A 必须沿着 next 链条一直往后走,直到找到真正的末尾。
校准 :一旦线程 A 完成了它的插入,它会尝试一次性将 tail 移动到它刚插入的那个最新节点上,瞬间完成“大幅度校准”。
3. 节点被删除导致的特殊偏离(Dangling Tail) 有一种特殊情况:tail 指向的节点已经被出队(poll)并从链表中逻辑删除了。
原理 :当一个节点出队后,为了帮助 GC,它的 next 可能会指向它自己(p.next = p)。
处理 :当 offer 操作发现 tail.next == tail 时,说明 tail 已经由于落后太多,掉进了“时空裂缝”(指向了一个已失效的节点)。此时,线程会直接跳到 head 指针处,从头开始重新寻找末尾。
CopyOnWriteArrayList CopyOnWriteArrayList 是 Java 并发包中设计最独特的集合之一。它的核心思想正如其名:写时复制(Copy-on-Write, COW) 。
其设计初衷是解决在多线程环境下,对 ArrayList 进行遍历时如果发生修改会抛出 ConcurrentModificationException 的问题,同时提供比完全加锁(Vector)更高的读取性能。
核心设计原理 CopyOnWriteArrayList 的核心逻辑可以概括为:读写分离,空间换时间 。
A. 内部结构 它内部维护了一个 volatile 修饰的数组 array。volatile 确保了当数组引用指向一个新的数组时,所有线程都能立即看到。
B. 写操作(修改、添加、删除) 当你执行 add() 或 remove() 时,它不会直接修改当前数组,而是执行以下步骤:
加锁 :获取一把独占锁(ReentrantLock),确保同一时刻只有一个线程在修改。
复制 :创建一个与当前数组内容完全相同的新数组。
修改 :在新数组 上执行添加或删除操作。
覆盖 :将内部的 array 引用指向这个新数组。
释放锁 :操作完成。
C. 读操作(get、遍历) 读操作是完全无锁 的。线程直接读取当前的 array。
由于写操作是在副本上进行的,读线程即使在写操作过程中进行遍历,读取的也是旧数组的快照(Snapshot),因此不会产生冲突,也不会抛出异常。
优点
线程安全且无锁读 :读操作性能极高,适合高频读取。
一致性快照 :遍历时不需要加锁,且不会受到修改操作的影响。
缺点
内存开销 :每次写操作都要复制整个数组。如果数组很大,会频繁触发 GC,甚至导致 OOM。
数据延迟(最终一致性) :读线程可能会在短时间内读取到旧数据,因为写操作还没把引用指向新数组。
写操作慢 :因为涉及数组拷贝,写性能远低于 ArrayList。
最佳实践场景:
读多写极少 :例如白名单、黑名单配置、系统属性配置。
监听器列表 :在 Android 中存储 Listener 或 Observer 集合。通常这些监听器在初始化时注册,运行期间频繁触发读取,极少修改。
与 Collections.synchronizedList 的区别
特性
CopyOnWriteArrayList
SynchronizedList
读性能
极高(无锁)
一般(有锁)
写性能
低(复制数组)
一般(锁竞争)
迭代器
弱一致性(Snapshot),不会报错
强一致性,修改会抛 Exception
内存占用
较高(写时翻倍)
低(原地修改)
附录 Java 7 ConcurrentHashMap Segment 分段锁 在 Java 7 的 ConcurrentHashMap 中,Segment 分段锁 的实现本质上是“化整为零” 的锁管理策略。它通过将一个巨大的哈希表拆分为多个独立维护的小型哈希表,从而实现真正的并发写入。
以下是其核心设计与实现逻辑:
1. 核心数据结构:分层架构 Segment 的实现依赖于一个三层嵌套结构:
外层 ConcurrentHashMap :持有一个 Segment 数组。
中层 Segment :每个 Segment 内部包含一个小型 HashEntry 数组。最关键的是,**Segment 类继承自 ReentrantLock**,这使得每个分段本身就是一把锁。
内层 HashEntry :具体的键值对节点。
2. 写入流程(put 操作):双重定位 当你向 Map 中插入数据时,会经历两次定位过程:
定位 Segment :通过对 Key 的 hashCode 进行再哈希,计算出该数据属于哪一个 Segment。
加锁操作 :调用该 Segment 的 lock() 方法。此时,只有操作同一个 Segment 的线程才会被阻塞 。操作不同 Segment 的线程可以并行执行。
定位 HashEntry :在 Segment 内部的数组中找到具体的桶位置,执行插入逻辑。
释放锁 :操作完成后通过 unlock() 释放。
3. 读取流程(get 操作):无锁化设计 Segment 设计的一个精妙之处在于 get 操作通常是不需要加锁 的。
实现原理 :HashEntry 的 value 字段和 next 指针都使用了 volatile 关键字 。
效果 :volatile 保证了内存可见性,即一个线程修改了数据,另一个线程能立刻看到最新值。这使得读取操作可以在不获取锁的情况下安全进行,极大提升了吞吐量。
4. 关键技术点:为什么是 2 的幂次方? 正如之前提到的,Segment 数组的大小(并发级别)会被调整为 2 的幂次方。
掩码运算 :假设 Segment 数量为 16(即 $2^4$),寻址时只需执行 (hash >>> segmentShift) & segmentMask。
效率 :这种位运算比传统的取模运算(%)要快得多,确保了在高并发下定位 Segment 的性能损耗降到最低。
5. 总结:Segment 的本质 Segment 实际上是对锁的物理隔离 。
它解决了 Hashtable 中“一人持锁,全家等待”的尴尬局面。
限制 :一旦初始化完成,Segment 的数量就固定了,这意味着它的最大并发度在整个生命周期内无法动态扩展。这也是为什么 Java 8 最终转向了基于 CAS 的桶级(Bucket-level)锁 ,实现了更细粒度的控制。
CAS原子操作原语 无锁软件通常依赖硬件提供的原子读取-修改-写入 (Read-Modify-Write)原语。
在较老一代的 ARM64 CPU 上,原子操作使用 LL/SC 循环 (Load-Link/Store-Conditional)。CPU 加载一个值并标记该地址。如果另一个线程写入了该地址,存储操作会失败,循环会重试。因为线程可以不断尝试并在不等待其他线程的情况下成功,所以这一操作是无锁的。
1 2 3 4 5 6 asmretry: ldxr x0, [x1] // 从地址 x1 独占加载到 x0 add x0, x0, #1 // 值加 1 stxr w2, x0, [x1] // 独占存储 // w2 为 0 表示成功,1 表示失败 cbnz w2, retry // 如果 w2 非零(失败),跳转到 retry
(在 Compiler Explorer 中查看)
更新的 ARM 架构(ARMv8.1)支持大型系统扩展 (Large System Extensions, LSE),包括 CAS(Compare-And-Swap)和 Load-And-Add 等指令。在 Android 17 中,我们为 ART 编译器添加了 LSE 检测支持,能够在支持 LSE 的设备上生成优化指令:
1 2 3 asm// ARMv8.1 LSE 原子操作示例 ldadd x0, x1, [x2] // 原子 load-add // 更快,无需循环
在谷歌的基准测试中,使用 CAS 的高竞争代码比 LL/SC 变体实现了约 3 倍加速 。
Java 语言通过 java.util.concurrent.atomic 提供原子操作原语,底层依赖这些特殊的 CPU 指令。