The art of multipropcessor programming 读书笔记-3. 自旋锁与争用(2)
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现。并根据个人的查资料以及理解的经历,给各位想更深入理解的人分享一些个人的资料
自旋锁与争用
3. 队列锁
之前实现的基于回退的锁,除了通用性以外,还有如下两个问题:
- CPU 高速缓存一致性流量:虽然由于回退存在,所以流量比 TASLock 要小,但是多线程访问锁的状态还是有一定因为缓存一致性导致的流量消耗的。
- 可能降低访问临界区的效率:由于所有线程的 sleep 延迟过大,导致当前所有线程都在 sleep,但是锁实际上已经释放。
可以将线程放入一个队列,来解决上面两个问题:
- 队列中,每个线程检查它的前驱线程是否已经完成,判断锁是否被释放,不用访问锁的状态。这样访问的是不同的内存,减少了锁释放修改状态导致的 CPU 高速缓存一致性流量
- 不需要 sleep,可以通过前驱线程告知线程锁被释放,尝试获取锁,提高了访问临界区的效率
最后,通过队列,也是实现了 FIFO 的公平性。
3.1. 基于数组的锁
我们通过一个数组来实现队列的功能,其流程是:
- 需要的存储:
- boolean 数组,为 true 则代表对应槽位的线程获取到了锁,为 false 则为对应槽位的线程没有获取到了锁
- 保存当前最新槽位的原子变量,每次上锁都会将这个原子变量加 1,之后对 boolean 数组的大小取余。这个值代表这个线程占用了 boolean 数组的这个位置,boolean 数组的这个位置的值代表这个线程是否获取到了锁。这也说明,boolean 数组的容量决定了这个锁同时可以有多少线程进行争用
- ThreadLocal,记录当前线程占用的 boolean 数组的位置
- 上锁流程:
- 原子变量 + 1,对 boolean 数组的大小取余得到 current
- 将 current 记录到 ThreadLocal
- 当 boolean 数组 cuurent 位置的值为 false 的时候,自旋等待
- 解锁流程:
- 从 ThreadLocal 中获取当前线程对应的位置 mine
- 将 boolean 数组的 mine 位置标记为 false
- 将 boolean 数组的 mine + 1 对数组大小取余的位置(防止数组越界)标记为 true
其源码是:
public class ArrayLock implements Lock {
private final ThreadLocal<Integer> mySlotIndex = ThreadLocal.withInitial(() -> 0);
private final AtomicInteger tail = new AtomicInteger(0);
private final boolean[] flags;
private final int capacity;
public ALock(int capacity) {
this.capacity = capacity;
this.flags = new boolean[capacity];
}
@Override
public void lock() {
int current = this.tail.getAndIncrement() % capacity;
this.mySlotIndex.set(current);
while (!this.flags[current]) {
}
}
@Override
public void unlock() {
int mine = this.mySlotIndex.get();
this.flags[mine] = false;
this.flags[(mine + 1) % capacity] = true;
}
}
在这个源码实现上,我们还可以做很多优化:
- 自旋等待可以不用强 Spin,而是 CPU 占用更低并且针对不同架构并且针对自旋都做了 CPU 指令优化的
Thread.onSpinWait()
。 - boolean 数组的每个槽位需要做缓存行填充,防止 CPU false sharing 的发生导致缓存行失效信号过多发布。
- boolean 数组的更新需要是 volatile 更新,普通更新会延迟总线信号,导致其他等带锁的线程感知的更慢从而空转更多次。
- 取余是非常低效的运算,需要转化为与运算,对 2 的 n 次方取余相当于对 2 的 n 次方减去 1 取与运算,我们需要将传入的 capacity 值转化为大于 capacity 最近的 2 的 n 次方的值来实现。
this.flags[current]
这个读取数组的操作需要放在循环外面,防止每次读取数组的性能消耗。
优化后的源码是:
public class ArrayLock implements Lock {
private final ThreadLocal<Integer> mySlotIndex = ThreadLocal.withInitial(() -> 0);
private final AtomicInteger tail = new AtomicInteger(0);
private final ContendedBoolean[] flags;
private final int capacity;
private static class ContendedBoolean {
//通过注解实现缓存行填充
@Contended
private boolean flag;
}
//通过句柄实现 volatile 更新
private static final VarHandle FLAG;
static {
try {
//初始化句柄
FLAG = MethodHandles.lookup().findVarHandle(ContendedBoolean.class, "flag", boolean.class);
} catch (Exception e) {
throw new Error(e);
}
}
public ArrayLock(int capacity) {
capacity |= capacity >>> 1;
capacity |= capacity >>> 2;
capacity |= capacity >>> 4;
capacity |= capacity >>> 8;
capacity |= capacity >>> 16;
capacity += 1; //大于N的最小的2的N次方
this.flags = new ContendedBoolean[capacity];
for (int i = 0; i < this.flags.length; i++) {
this.flags[i] = new ContendedBoolean();
}
this.capacity = capacity;
this.flags[0].flag = true;
}
@Override
public void lock() {
int current = this.tail.getAndIncrement() & (capacity - 1);
this.mySlotIndex.set(current);
ContendedBoolean contendedBoolean = this.flags[current];
while (!contendedBoolean.flag) {
Thread.onSpinWait();
}
}
@Override
public void unlock() {
int mine = this.mySlotIndex.get();
FLAG.setVolatile(this.flags[mine], false);
FLAG.setVolatile(this.flags[(mine + 1) & (capacity - 1)], true);
}
}
但是,即使有这些优化,在高并发大量锁调用的时候,这个锁的性能依然会很差。这个我们之后会分析优化。
The art of multipropcessor programming 读书笔记-3. 自旋锁与争用(2)的更多相关文章
- The art of multipropcessor programming 读书笔记-硬件基础1
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...
- The art of multipropcessor programming 读书笔记-硬件基础2
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...
- The Art of Multiprocessor Programming读书笔记 (更新至第3章)
这份笔记是我2013年下半年以来读“The Art of Multiprocessor Programming”这本书的读书笔记.目前有关共享内存并发同步相关的书籍并不多,但是学术文献却不少,跨越的时 ...
- 《高性能MySQL》读书笔记--锁、事务、隔离级别 转
1.锁 为什么需要锁?因为数据库要解决并发控制问题.在同一时刻,可能会有多个客户端对表中同一行记录进行操作,比如有的在读取该行数据,其他的尝试去删除它.为了保证数据的一致性,数据库就要对这种并发操作进 ...
- 【MySQL 读书笔记】全局锁 | 表锁 | 行锁
全局锁 全局锁是针对数据库实例的直接加锁,MySQL 提供了一个加全局锁的方法, Flush tables with read lock 可以使用锁将整个表的增删改操作都锁上其中包括 ddl 语句,只 ...
- Head First HTML5 Programming 读书笔记
1:HTML5引入了简单化的标记,新的语义和媒体元素,另外要依赖于一组支持web应用的js库. 2:关于js 对象是属性的结合 window对象是全局变量. document对象是window的一个属 ...
- 《java并发编程实战》读书笔记10--显示锁Lock,轮询、定时、读写锁
第13章 显示锁 终于看到了这本书的最后一本分,呼呼呼,真不容易.其实说实在的,我不喜欢半途而废,有其开始,就一定要有结束,否则的话就感觉哪里乖乖的. java5.0之前,在协调对共享对象的访问时可以 ...
- 《高性能MySQL》读书笔记之 MySQL锁、事务、多版本并发控制的基础知识
1.2 并发控制 1.2.1 读写锁 在处理并发读或写时,通过实现一个由两种类型的锁组成的锁系统来解决问题.这两种类型的锁通常被称为 共享锁(shared lock) 和 排它锁(exclusive ...
- 《Programming Hive》读书笔记(一)Hadoop和hive环境搭建
<Programming Hive>读书笔记(一)Hadoop和Hive环境搭建 先把主要的技术和工具学好,才干更高效地思考和工作. Chapter 1.Int ...
随机推荐
- VMware安装最新版CentOS7图文教程
https://blog.csdn.net/reticent_man/article/details/80732395 https://blog.csdn.net/q2158798/article/d ...
- WireShark高级用法
报文注释 分组注释 尽量使用英文注释 时间显示 显示实际时间 抓包过滤器 按照规则抓取报文 显示过滤器 按照规则显示报文 自动生成过滤条件:做为过滤器应用 着色规则 默认 可自定义着色规则 追踪数据流 ...
- python学习笔记(五)-文件操作2
一.文件修改 现有文件file.txt,内容如下:二十四节气歌春雨惊春清谷天,夏满芒夏暑相连.秋处露秋寒霜降,冬雪雪冬小大寒.上半年逢六廿一,下半年逢八廿三.每月两节日期定,最多相差一二天.要求:将文 ...
- c++ 的学习 第二集函数的重载2 namemangling
1. 本质 采用了name mangling或者叫name decoration ✓ C++编译器默认会对符号名(比如函数名)进行改编.修饰,有些地方翻译为"命名倾轧"✓ 重载时 ...
- 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班? | 百篇博客分析OpenHarmony源码 | v22.01
百篇博客系列篇.本篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在 ...
- AT2368-[AGC013B]Hamiltonish Path【构造】
正题 题目链接:https://www.luogu.com.cn/problem/AT2368 题目大意 给出 \(n\) 个点 \(m\) 条边的一张无向图,然后求一条路径满足 路径长度不小于二. ...
- 自然语言处理标注工具——Brat(安装、测试、使用)
一.Brat标注工具安装 1.安装条件: (1)运行于Linux系统(window系统下虚拟机内linux系统安装也可以) (2)目前brat最新版本(v1.3p1)仅支持python2版本运行使用( ...
- c++ fstream feekg讨论
#include <iostream> #include <fstream> using namespace std; int main() { std::ifstream f ...
- SphereEx 创始人张亮云咖访谈回顾:构建数据服务的新思路
2021 年 7 月 21 日,2021 亚马逊云科技中国峰会在上海盛大开幕.本次大会以"构建新格局,共赢云时代"为主题,邀请到来自技术社区.开源软件基金会.开源创业代表.女性开发 ...
- mysql order by语句流程是怎么样的
order by流程是怎么样的 注意点: select id, name,age,city from t1 where city='杭州' order by age limit 1000; order ...