The art of multipropcessor programming 读书笔记-硬件基础1
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现。并根据个人的查资料以及理解的经历,给各位想更深入理解的人分享一些个人的资料
硬件基础
首先,我们无需掌握大量的底层计算机系统结构设计知识的细节,如果大家对于计算机系统结构非常感兴趣,那么我非常推荐大家搜一下 CSE 502 Computer Architecture 的课件,我看了这门课的 CPU 和 缓存章节,受益匪浅。这里列出的硬件基础知识,主要为了能让我们理解编程语言为了提高性能做的设计。
我们一般能轻松写出来适合单处理器运行的代码,但是面对多处理器的情况,可能同样的代码效率就低很多,请看下面这个例子:假设两个线程需要访问一个资源,这个资源不能被多线程同时访问,访问前必须上锁,拿到锁之后才能访问这个资源,并且需要在访问完资源后释放锁。
我们这里使用一个 boolean 域实现锁,如果这个 boolean 为 false 则锁是空闲状态,否则就是正在被使用。使用 compareAndSet 来获取锁,这个调用返回 true 就是修改成功,即获取锁成功,返回 false 则修改失败,即获取锁失败。假设我们有如下这个锁的接口:
public interface Lock {
void lock();
void unlock();
}
对于这个锁,我们有以下两种实现,第一个是:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class TASLock implements Lock {
private boolean locked = false;
//操作 lock 的句柄
private static final VarHandle LOCKED;
static {
try {
//初始化句柄
LOCKED = MethodHandles.lookup().findVarHandle(TASLock.class, "locked", boolean.class);
} catch (Exception e) {
throw new Error(e);
}
}
@Override
public void lock() {
// compareAndSet 成功代表获取了锁
while(!LOCKED.compareAndSet(this, false, true)) {
//让出 CPU 资源,这是目前实现 SPIN 效果最好的让出 CPU 的方式,当线程数量远大于 CPU 数量时,效果比 Thread.yield 好,从及时性角度效果远好于 Thread.sleep
Thread.onSpinWait();
}
}
@Override
public void unlock() {
//需要 volatile 更新,让其他线程感知到
LOCKED.setVolatile(this, false);
}
}
另一个锁的实现是:
public class TTASLock implements Lock {
private boolean locked = false;
//操作 locked 的句柄
private static final VarHandle LOCKED;
static {
try {
//初始化句柄
LOCKED = MethodHandles.lookup().findVarHandle(TTASLock.class, "locked", boolean.class);
} catch (Exception e) {
throw new Error(e);
}
}
@Override
public void lock() {
while (true) {
//普通读取 locked,如果被占用,则一直 SPIN
while ((boolean) LOCKED.get(this)) {
//让出 CPU 资源,这是目前实现 SPIN 效果最好的让出 CPU 的方式,当线程数量远大于 CPU 数量时,效果比 Thread.yield 好,从及时性角度效果远好于 Thread.sleep
Thread.onSpinWait();
}
//成功代表获取了锁
if (LOCKED.compareAndSet(this, false, true)) {
return;
}
}
}
@Override
public void unlock() {
LOCKED.setVolatile(this, false);
}
}
为了灵活性和统一,我们两个锁都是使用了句柄,而不是 volatile 变量或者是 AtomicBoolean,因为我们在这两个锁中会有 volatile 更新,普通读取,以及原子更新;如果读者不习惯,可以使用 AtomicBoolean 近似代替。
接下来我们使用 JMH 测试这两个锁的性能以及有效性,我们将一个 int 类型的变量使用多线程加 500 万次,并且使用我们实现的这两个锁确保并发安全,查看耗时。
//测试指标为单次调用时间
@BenchmarkMode(Mode.SingleShotTime)
//需要预热,排除 jit 即时编译以及 JVM 采集各种指标带来的影响,由于我们单次循环很多次,所以预热一次就行
@Warmup(iterations = 1)
//单线程即可
@Fork(1)
//测试次数,我们测试10次
@Measurement(iterations = 10)
//定义了一个类实例的生命周期,所有测试线程共享一个实例
@State(value = Scope.Benchmark)
public class Test {
private static class ValueHolder {
int count = 0;
}
//测试不同线程数量
@Param(value = {"1", "2", "5", "10", "20", "50", "100"})
private int threadsCount;
@Benchmark
public void testTASLock(Blackhole blackhole) throws InterruptedException {
test(new TASLock());
}
@Benchmark
public void testTTASLock(Blackhole blackhole) throws InterruptedException {
test(new TTASLock());
}
private void test(Lock lock) throws InterruptedException {
ValueHolder valueHolder = new ValueHolder();
Thread[] threads = new Thread[threadsCount];
//测试累加 5000000 次
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 5000000 / threads.length; j++) {
lock.lock();
try {
valueHolder.count++;
} finally {
lock.unlock();
}
}
});
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
if (valueHolder.count != 5000000) {
throw new RuntimeException("something wrong in lock implementation");
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(Test.class.getSimpleName()).build();
new Runner(opt).run();
}
}
结果是:
Benchmark (threadsCount) Mode Cnt Score Error Units
Test.testTASLock 1 ss 10 0.087 ± 0.012 s/op
Test.testTASLock 2 ss 10 0.380 ± 0.145 s/op
Test.testTASLock 5 ss 10 0.826 ± 0.310 s/op
Test.testTASLock 10 ss 10 1.698 ± 0.563 s/op
Test.testTASLock 20 ss 10 2.897 ± 0.699 s/op
Test.testTASLock 50 ss 10 5.448 ± 2.513 s/op
Test.testTASLock 100 ss 10 7.900 ± 5.011 s/op
Test.testTTASLock 1 ss 10 0.083 ± 0.003 s/op
Test.testTTASLock 2 ss 10 0.353 ± 0.067 s/op
Test.testTTASLock 5 ss 10 0.543 ± 0.123 s/op
Test.testTTASLock 10 ss 10 0.743 ± 0.356 s/op
Test.testTTASLock 20 ss 10 1.437 ± 0.161 s/op
Test.testTTASLock 50 ss 10 1.926 ± 0.769 s/op
Test.testTTASLock 100 ss 10 2.428 ± 0.878 s/op
可以看出,这两个锁虽然逻辑上是等价的,但是性能上确有很大差异,并且随着线程的增加,这个差异越来越大,如下图所示:

本章为了解释这个问题,会涵盖要写出高效的并发算法和数据结构所需要的关于多处理器系统结构的大多数知识。我们会考虑如下这些组件:
- 处理器(processors):执行软件线程(threads)的设备。通常情况下,线程数远大于处理器个数,处理器需要切换,即运行一个线程一段时间之后切换到另一个线程运行。
- 互连线(interconnect):连接处理器与处理器或者处理器与内存。
- 内存(memory):具有层次的存储数据的组件,包括多层高速缓存和速度相对较慢的大容量主内存。理解这些层次之间的相互关系是理解许多并发算法实际性能的基础。
The art of multipropcessor programming 读书笔记-硬件基础1的更多相关文章
- The art of multipropcessor programming 读书笔记-硬件基础2
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...
- The art of multipropcessor programming 读书笔记-3. 自旋锁与争用(2)
本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...
- The Art of Multiprocessor Programming读书笔记 (更新至第3章)
这份笔记是我2013年下半年以来读“The Art of Multiprocessor Programming”这本书的读书笔记.目前有关共享内存并发同步相关的书籍并不多,但是学术文献却不少,跨越的时 ...
- Head First HTML5 Programming 读书笔记
1:HTML5引入了简单化的标记,新的语义和媒体元素,另外要依赖于一组支持web应用的js库. 2:关于js 对象是属性的结合 window对象是全局变量. document对象是window的一个属 ...
- Clr Via C#读书笔记---线程基础
趣闻:我是一个线程:http://kb.cnblogs.com/page/542462/ 进程与线程 进程:应用程序的一个实例使用的资源的集合.每个进程都被赋予了一个虚拟地址空间. 线程:对CPU进行 ...
- 3D数学读书笔记——矩阵基础
本系列文章由birdlove1987编写,转载请注明出处. 文章链接:http://blog.csdn.net/zhurui_idea/article/details/24975031 矩 ...
- PILE读书笔记_基础知识
程序的构成 Linux下二进制可执行程序的格式一般为ELF格式. 我们可以用readelf命令来读取二进制的信息. ELF文件的主要内容就是由各个section及symbol表组成的. 下面来分别介绍 ...
- 3D数学读书笔记——矩阵基础番外篇之线性变换
本系列文章由birdlove1987编写.转载请注明出处. 文章链接:http://blog.csdn.net/zhurui_idea/article/details/25102425 前面有一篇文章 ...
- Javascript DOM 编程艺术(第二版)读书笔记——DOM基础
1.DOM是什么 D=document(文档) O=object(对象) M=Model(模型) DOM又称节点树 一些术语: parent(父) child(子) sibling(兄弟) ...
随机推荐
- SpringBoot快速入门(必知必会)
是什么?能做什么 SpringBoot必知必会 是什么?能做什么 SpringBoot是一个快速开发脚手架 快速创建独立的.生产级的基于Spring的应用程序 SpringBoot必知必会 快速创建应 ...
- Linux centos 安装 jenkins & 本地构建jar & 远程构建jar
一.部署 jenkins 需要的前奏 1.安装 JDK:https://www.cnblogs.com/chuyi-/p/10644440.html 2.安装tomcat:https://www.cn ...
- 详细分析MySQL事务日志(undo log)
2.undo log 2.1 基本概念 undo log有两个作用:提供回滚和多个行版本控制(MVCC). 在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败 ...
- JSTL标签报错-http://java.sun.com/jsp/jstl/core
考虑为tomcat缺少相关的包 导入就好了 导入jstl-api-1.2.jar 以及standard-1.1.2.jar 然后重启服务 更多java学习,请进本人小博客-https://zhangj ...
- 分布式系列-分布式ID
一.数据库自增(单实例) 1.方案描述 基于数据库自增ID(auto_increment)利用其来充当分布式ID.实现方式就是用一张表来充当ID生成器,当我们需要ID时,向表中插入一条记录返回主键ID ...
- 性能环境之docker操作指南4(全网最全)
容器的常用操作 docker run -i -t /bin/bash 使用image创建container并进入交互模式, login shell是/bin/bash 实例: $ docker ru ...
- SpringBoot快速集成SpringBootAdmin管控台监控服务
SpringBootAdmin是一个针对 Spring Boot 的 Actuator 接口进行 UI 美化封装的监控工具,它可以在列表中浏览所有被监控 spring-boot 项目的基本信息.详细的 ...
- Django——Auth模块(用户认证模块)
1.Auth模块简介 auth模块是对登录认证方法的一种封装,之前我们获取用户输入的用户名及密码后需要自己从user表里查询有没有用户名和密码符合的对象. 而有了auth模块之后就可以很轻松的去验证用 ...
- Dart简易教程 (1)---数据类型 运算符,类转换换
从下面开始学习DART编程 以下是一个简单的示例: main(){ var number = 42; print(number);}程序说明,dart是一个强大的脚本类语言,可以不预先定义变量类型 , ...
- JS预编译过程
GO和AO 变量的预编译 实例1 console.log(a); var a=1; console.log(a); 实际编译过程: 将a存入预编译对象中,赋值为undefined: 真正的赋值语句当程 ...