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 ...
随机推荐
- Linux系列(25) - 常用快捷键(未更新完)
快捷键 说明 Ctrl+L 清屏 tab tab按一次自动补全目录文件名称/tab按二次将目录下带有补全前面字段的所有文件目录展示出来,例子: cd / tab键按两次将根目录下所有文件展示出来 ...
- Xftp乱码问题
Xftp出现乱码 修改编码
- java 163邮箱验证
第一步:引入工具类 import java.util.Properties; import javax.mail.Authenticator; import javax.mail.Message; i ...
- centos7 .net core 使用supervisor守护进程后台运行
安装supervisor yum install supervisor 配置supervisor vi /etc/supervisord.conf 拉到最后,这里的意思是 /etc/superviso ...
- Elasticsearch、XXLJob以及最近的学习记录
Elasticsearch.XXLJob以及最近的学习记录 前言 在这九月的最后一周,来总结一下最近的学习记录,主要是对于Elasticsearch.XXLjob的初步学习,想着还是多记录点,以便后面 ...
- 实验3:OpenFlow协议分析实践
作业链接:实验3:OpenFlow协议分析实践 一.实验目的 能够运用 wireshark 对 OpenFlow 协议数据交互过程进行抓包: 能够借助包解析工具,分析与解释 OpenFlow协议的数据 ...
- 1-基本建表sql语句
基本的建表语句的总结 --建表语法 CREATE TABLE 表名( --约束可以没有 列名1 数据类型 [约束], 列名2 数据类型 [约束], ......, [约束], ..... ); --该 ...
- 实时获取股票数据,免费!——Python爬虫Sina Stock实战
更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流. 实时股票数据的重要性 对于四大可交易资产:股票.期货.期权.数字货币来说,期货.期权.数字货币,可以从交 ...
- Java基础之(六):变量、运算符与JavaDoc
变量.常量 一.变量的命名规范 首字母只能以字母(A-Z或者a-z)或者美元符($)或者下划线(_)开头,不能以数字开头,首字母之后只能跟字母(AZ或者az)或者数字,不能跟美元符或者下划线 源码 p ...
- ElasticSearch IK热词自动热更新原理与Golang实现
热更新概述 ik分词器本身可以从配置文件加载扩张词库,也可以从远程HTTP服务器加载. 从本地加载,则需要重启ES生效,影响比较大.所以,一般我们都会把词库放在远程服务器上.这里主要有2种方式: 借助 ...