本系列是 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;
}
}

在这个源码实现上,我们还可以做很多优化:

  1. 自旋等待可以不用强 Spin,而是 CPU 占用更低并且针对不同架构并且针对自旋都做了 CPU 指令优化的 Thread.onSpinWait()
  2. boolean 数组的每个槽位需要做缓存行填充,防止 CPU false sharing 的发生导致缓存行失效信号过多发布。
  3. boolean 数组的更新需要是 volatile 更新,普通更新会延迟总线信号,导致其他等带锁的线程感知的更慢从而空转更多次。
  4. 取余是非常低效的运算,需要转化为与运算,对 2 的 n 次方取余相当于对 2 的 n 次方减去 1 取与运算,我们需要将传入的 capacity 值转化为大于 capacity 最近的 2 的 n 次方的值来实现。
  5. 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)的更多相关文章

  1. The art of multipropcessor programming 读书笔记-硬件基础1

    本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...

  2. The art of multipropcessor programming 读书笔记-硬件基础2

    本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...

  3. The Art of Multiprocessor Programming读书笔记 (更新至第3章)

    这份笔记是我2013年下半年以来读“The Art of Multiprocessor Programming”这本书的读书笔记.目前有关共享内存并发同步相关的书籍并不多,但是学术文献却不少,跨越的时 ...

  4. 《高性能MySQL》读书笔记--锁、事务、隔离级别 转

    1.锁 为什么需要锁?因为数据库要解决并发控制问题.在同一时刻,可能会有多个客户端对表中同一行记录进行操作,比如有的在读取该行数据,其他的尝试去删除它.为了保证数据的一致性,数据库就要对这种并发操作进 ...

  5. 【MySQL 读书笔记】全局锁 | 表锁 | 行锁

    全局锁 全局锁是针对数据库实例的直接加锁,MySQL 提供了一个加全局锁的方法, Flush tables with read lock 可以使用锁将整个表的增删改操作都锁上其中包括 ddl 语句,只 ...

  6. Head First HTML5 Programming 读书笔记

    1:HTML5引入了简单化的标记,新的语义和媒体元素,另外要依赖于一组支持web应用的js库. 2:关于js 对象是属性的结合 window对象是全局变量. document对象是window的一个属 ...

  7. 《java并发编程实战》读书笔记10--显示锁Lock,轮询、定时、读写锁

    第13章 显示锁 终于看到了这本书的最后一本分,呼呼呼,真不容易.其实说实在的,我不喜欢半途而废,有其开始,就一定要有结束,否则的话就感觉哪里乖乖的. java5.0之前,在协调对共享对象的访问时可以 ...

  8. 《高性能MySQL》读书笔记之 MySQL锁、事务、多版本并发控制的基础知识

    1.2 并发控制 1.2.1 读写锁 在处理并发读或写时,通过实现一个由两种类型的锁组成的锁系统来解决问题.这两种类型的锁通常被称为 共享锁(shared lock) 和 排它锁(exclusive ...

  9. 《Programming Hive》读书笔记(一)Hadoop和hive环境搭建

    <Programming Hive>读书笔记(一)Hadoop和Hive环境搭建             先把主要的技术和工具学好,才干更高效地思考和工作.   Chapter 1.Int ...

随机推荐

  1. mongodb linux基本启动 基础增删改 mysql语法的对比

    一.主流数据源类型 还存在自定义数据源以及REST接口数据,共6中数据源. 二.linux下启动连接数据库 进去mongodb的目录启动服务:mongo --host 192.168.320.826 ...

  2. 一文让你彻底理解group by和聚合函数

    知道group by是进行分组查询,但是一直觉得对其理解得不够透彻,在网上扒了一篇文章,我认为写得非常好. 为什么不能够select * from Table group by id,为什么一定不能是 ...

  3. centos7.5 部署 lopendap

    一.LDAP究竟是什么? LDAP是轻量目录访问协议,英文全称是Lightweight Directory Access Protocol,一般都简称为LDAP.它是基于X.500标准的,但是简单多了 ...

  4. html正文提取工具goose的安装及简单使用Demo

    1.git clone https://github.com/grangier/python-goose.git 2.cd python-goose 3.sudo pip install -r req ...

  5. iSCSI 服务器搭建

    一.简介 SCSI(Small Computer System Interface),小型计算机系统接口,是一种用于计算机及其周边设备之间(硬盘.软驱.光驱.打印机.扫描仪等)系统级接口的独立处理器标 ...

  6. VMware vSphere 7 Update 3 下载

    请访问原文链接:https://sysin.org/blog/vmware-vsphere-7-u3/,查看最新版.原创作品,转载请保留出处. vSphere 7 Update 3 已经宣布可用,即将 ...

  7. 11.4.2 LVS—NAT

    Virtual Server via NAT(VS-NAT) 用地址翻译实现虚拟服务器。地址转换器有能被外界访问到的合法IP地址,它修改来自专有网络的流出包的地址。外界看起来包是来自地址转换器本身,, ...

  8. SQL SERVER数据库权限分配

    1,新建 只能访问某一个表的只读用户. --添加只允许访问指定表的用户:         exec     sp_addlogin     '用户名','密码','默认数据库名'           ...

  9. 洛谷T31018 经典题丶改(LCT+离线)

    真的是一个大好题啊! QWQ首先我们考虑这种问题,如果直接在线做,估计应该是做不了,那我们是不是可以直接考虑离线. 将所有询问都按照\(r\)来排序. 然后依次加入每条边,计算\(a[i]<=n ...

  10. FastAPI 学习之路(二十)接口文档配置相关

    系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) FastAPI 学习之路(四) FastAPI 学习之 ...