本系列是 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. Java基础系列(38)- 数组的使用

    数组的使用 For-Each循环 数组作方法入参 数组作返回值 For-Each循环 普通型 package array; import sun.security.util.Length; publi ...

  2. javascript 数组 shuffle 洗牌 打乱顺序

    * php shuffle 打乱数组顺序 Array.prototype.shuffle = function () { "use strict"; var a = [], b = ...

  3. AT4353-[ARC101D]Robots and Exits【LIS】

    正题 题目链接:https://www.luogu.com.cn/problem/AT4353 题目大意 数轴上有\(n\)个球\(m\)个洞,每次可以将所有球左移或者右移,球到洞的位置会掉下去. 求 ...

  4. Loj#116-[模板]有源汇有上下界最大流

    正题 题目链接:https://loj.ac/p/116 题目大意 \(n\)个点\(m\)条边的一张图,每条边有流量上下限制,求源点到汇点的最大流. 解题思路 先别急着求上面那个,考虑一下怎么求无源 ...

  5. 牛客挑战赛48C-铬合金之声【Prufer序列】

    正题 题目链接:https://ac.nowcoder.com/acm/contest/11161/C 题目大意 \(n\)个点加\(m\)条边使得不存在环,每种方案的权值是所有联通块的大小乘积. 求 ...

  6. SpringBoot之SpringSecurity权限注解在方法上进行权限认证多种方式

    前言 Spring Security支持方法级别的权限控制.在此机制上,我们可以在任意层的任意方法上加入权限注解,加入注解的方法将自动被Spring Security保护起来,仅仅允许特定的用户访问, ...

  7. k8s deployment controller源码分析

    deployment controller简介 deployment controller是kube-controller-manager组件中众多控制器中的一个,是 deployment 资源对象的 ...

  8. 左手IRR,右手NPV,掌握发家致富道路密码

    智能手机的普及让世界成为了我们指尖下的方寸之地. 在各种信息爆炸出现的同时,五花八门的理财信息与我们的生活越贴越近.投资不再仅仅是企业行为,对于个人而言,也是很值得关注的内容. 但是落脚到很小的例子之 ...

  9. js Promise用法

    Promise 英文意思是 承诺的意思,是对将来的事情做了承诺, Promise 有三种状态, Pending 进行中或者等待中 Fulfilled 已成功 Rejected 已失败 Promise ...

  10. linux下nginx编译安装、版本信息修改

    环境 centos 7 安装依赖包 yum install -y gcc gcc-c++ glibc glibc-devel pcre pcre-devel zlib zlib-devel opens ...