本系列是 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. MySql分区、分表和分库

    MySql分区.分表和分库 数据库的数据量达到一定程度之后,为避免带来系统性能上的瓶颈.需要进行数据的处理,采用的手段是分区.分片.分库.分表. 一些问题的解释: 1.为什么要分表和分区? 日常开发中 ...

  2. spring入门1-IOC和DI

    1.概述 1.1.简介 Spring是分层的 Java SE/EE应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control:反转控制)和 AOP(Aspect Or ...

  3. contos 7修改root密码

    https://www.linuxidc.com/Linux/2018-01/150211.htm 下面是CentOS 7的root密码修改 开机按esc 选择CentOS Linux (3.10.0 ...

  4. Java学习之随堂笔记系列——day01

    学习方法:听.问.敲.悟听:前提:上课要听懂,没有听懂可以及时的问.问:任何的问题都要及时的问.敲:重点内容.多写多练,只有写和练习才能发现新的问题,有问题就问.悟:举一反三.提升自己.今日内容:1. ...

  5. 监控linux服务器工具nmon的使用

    做压测时,需要查看服务器中的cpu.内存变化,但由于服务器是linux环境,则需要监控linux服务器的工具,下面用到的工具是nmon. 1.安装nmon.在网上下载nmon安装包,在linux服务器 ...

  6. P3291-[SCOI2016]妖怪【凸壳】

    正题 题目链接:https://www.luogu.com.cn/problem/P3291 题目大意 给出 \(n\) 个数字对 \((atk,dnf)\),求一个\((a,b)\). 对于每个数字 ...

  7. YbtOJ#912-神秘语言【结论,欧拉定理】

    正题 题目链接:http://www.ybtoj.com.cn/problem/912 题目大意 给出\(L,R\),求有多少长度在\([L,R]\)之间的字符串满足依次取出所有偶数位置的放在最前面后 ...

  8. Python3入门系列之-----异常处理

    前言 作为 Python 初学者,在刚学习 Python 编程时,经常会看到一些报错信息,在前面我们没有提及,这章节我们会专门介绍. Python 有两种错误很容易辨认:语法错误和异常. Python ...

  9. MyBatis封装对象内的List出现的问题

    本篇文章问题1:wife的复数形式是wives,不是wifes,英语不好请见谅. 对象举例: class User { private String username; private List< ...

  10. python操作Redis方法速记

    redis有5种数据结构,分别如下: 5种数据结构 python语言对5种数据结构的增删改查 全局函数 redis 连接 import redis pool = redis.ConnectionPoo ...