关于并发:随机高并发查询结果一致性设计实践

作者:京东物流 赵帅 姚再毅 王旭东 孟伟杰 孔祥东 1 前言物流合约核心是京东物流合同治理的惟一入口。为商家提供合同的创立,盖章等能力,为不同业务条线提供合同的定制,归档,查问等性能。因为各个业务条线泛滥,为各个业务条线提供高可用查问能力是物流合约核心重中之重。同时计费零碎在每个物流单结算时,都须要查问合约核心,确保商家签订的合同内容来保障计费的准确性。 2 业务场景1.查问维度剖析从业务调用的起源来看,合同的大部分是计费零碎在每个物流单计费的时候,须要调用合约核心来判断,该商家是否签订合同。 从业务调用的入参来看,绝大部分是多个条件来查问合同,但根本都是查问某个商家,或通过商家的某个属性(例如业务账号)来查问合同。 从调用的后果来看,40%的查问是没有后果的,其中绝大部分是因为商家没有签订过合同,导致查问为空。其余的查问后果,每次返回的数量较少,个别一个商家只有3到5个合同。 2.调用量分析调用量目前合同的调用量,大略是在每天2000W次。 一天的调用量统计: 调用工夫每天高峰期为上班时间,最高峰为4W/min。 一个月的调用量统计: 由上能够看出,合同每日的调用量比拟均匀,次要集中在9点到12点和13点到18点,也就是上班时间,整体调用量较高,根本不存在调用暴增的状况。 总体剖析来看,合约核心的查问,调用量较高,且较均匀,根本都是随机查问,也并不存在热点数据,其中有效查问占比拟多,每次查问条件较多,返回数据量比不大。 3 方案设计从整体业务场景剖析来看,咱们决定做三层防护来保障调用量的撑持,同时须要对数据一致性做好解决。第一层是布隆过滤器,来拦挡绝大部分有效的申请。第二层是redis缓存数据,来保障各种查问条件的查问尽量命中redis。第三层是间接查询数据库的兜底计划。同时再保证数据一致性的问题,咱们借助于播送mq来实现。 1.第一层防护因为近一半的查问都是空,咱们首先这是缓存穿透的景象。 缓存穿透问题 缓存穿透(cache penetration)是用户拜访的数据既不在缓存当中,也不在数据库中。出于容错的思考,如果从底层数据库查问不到数据,则不写入缓存。这就导致每次申请都会到底层数据库进行查问,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻打时,数据库的压力骤增,甚至解体,这就是缓存穿透问题。 惯例解决方案 缓存特定值 个别对于缓存穿透咱们比拟惯例的做法就是,将不存在的key 设置一个固定值,比如说NULL,&&等等,在查问返回这个值的时候,咱们利用就能够认为这是一个不存在的key,那咱们利用就能够决定是否持续期待,还是持续拜访,还是间接放弃,如果持续期待拜访的话,设置一个轮询工夫,再次申请,如果取到的值不再是咱们预设的,那就代表曾经有值了,从而防止了透传到数据库,从而把大量的相似申请挡在了缓存之中。 缓存特定值并同步更新 特定值做了缓存,那就意味着须要更多的内存存储空间。当存储层数据变动了,缓存层与存储层的数据会不统一。有人会说,这个问题,给key 加上一个过期工夫不就能够了,的确,这样是最简略的,也能在肯定水平上解决这两个问题,然而当并发比拟高的时候(缓存并发),其实我是不倡议应用缓存过期这个策略的,我更心愿缓存始终存在;通过后盾零碎来更新缓存中的数据一致性的目标。 布隆过滤器 布隆过滤器的核心思想是这样的,它不保留理论的数据,而是在内存中建设一个定长的位图用0,1来标记对应数据是否存在零碎;过程是将数据通过多个哈希函数计算出不同的哈希值,而后用哈希值对位图的长度进行取模,最初失去位图的下标位,而后在对应的下标位上进行标记;找数的时候也是一样,先通过多个哈希函数失去哈希值,而后哈希值与位图的长度进行取模失去多个下标。如果多个下标都被标记成1了,那么阐明数据存在于零碎,不过只有有一个下标为0那么就阐明该数据必定不存在于零碎中。 在这里先通过一个示例介绍一下布隆过滤器的场景: 以ID查问文章为例,如果咱们要晓得数据库是否存在对应的文章,那么最简略的形式就是咱们把所有数据库存在的ID都保留到缓存去,这个时候当申请过进入零碎,先从这个缓存数据里判断零碎是否存在对应的数据ID,如果不存在的话间接返回进来,防止申请进入到数据库层,存在的话再从获取文章的信息。然而这个不是最好的形式,因为当文章的数量很多很多的时候,那缓存中就须要存大量的文档id而且只能持续增长,所以咱们得想一种形式来节俭内存资源当又能是申请都能命中缓存,这个就是布隆过滤器要做的。 咱们剖析布隆过滤器的优缺点 长处 1.不须要存储数据,只用比特示意,因而在空间占用率上有微小的劣势2.检索效率高,插入和查问的工夫复杂度都为 O(K)(K 示意哈希函数的个数)3.哈希函数之间互相独立,能够在硬件指令档次并行计算,因而效率较高。 毛病 1.存在不确定的因素,无奈判断一个元素是否肯定存在,所以不适宜要求 100% 准确率的场景2.只能插入和查问元素,不能删除元素。 布隆过滤器剖析:面对长处,完全符合咱们的诉求,针对毛病1,会有极少的数据穿透对系统来说并无压力。针对毛病2,合同的数据,原本就是不可删除的。如果合同过期,咱们能够查出单个商家的所有合同,从合同的完结工夫来判断合同是否无效,并不需要取删除布隆过滤器里的元素。 思考到调用redis布隆过滤器,会走一次网络,而咱们的查问近一半都是有效查问,咱们决定应用本地布隆过滤器,这样就能够缩小一次网络申请。然而如果是本地布隆过滤器,在更新时,就须要对所有机器的本地布隆过滤器更新,咱们监听合同的状态来更新,通过mq的播送模式,来对布隆过滤器插入元素,这样就做到了所有机器上的布隆过滤器对立元素插入。 2.第二层防护面对高并发,咱们首先想到的是缓存。 引入缓存,咱们就要思考缓存穿透,缓存击穿,缓存雪崩的三大问题。 其中缓存穿透,咱们已再第一层防护中解决,这里只解决缓存击穿,缓存雪崩的问题。 缓存击穿(Cache Breakdown)缓存雪崩是指只大量热点key同时生效的状况,如果是单个热点key,在不停的扛着大并发,在这个key生效的霎时,继续的大并发申请就会击破缓存,间接申请到数据库,如同蛮力击穿一样。这种状况就是缓存击穿。 惯例解决方案 缓存生效扩散 这个问题其实比拟好解决,就是在设置缓存的时效工夫的时候减少一个随机值,例如减少一个1-3分钟的随机,将生效工夫扩散开,升高个体生效的概率;把过期工夫管制在零碎低流量的时间段,比方凌晨三四点,避过流量的高峰期。 加锁 加锁,就是在查问申请未命中缓存时,查询数据库操作前进行加锁,加锁后前面的申请就会阻塞,防止了大量的申请集中进入到数据库查问数据了。 永恒不生效 咱们能够不设置过期工夫来保障缓存永远不会生效,而后通过后盾的线程来定时把最新的数据同步到缓存里去 解决方案:应用分布式锁,针对同一个商家,只让一个线程构建缓存,其余线程期待构建缓存执行结束,从新从缓存中获取数据。 缓存雪崩(Cache Avalanche)当缓存中大量热点缓存采纳了雷同的实效工夫,就会导致缓存在某一个时刻同时实效,申请全副转发到数据库,从而导致数据库压力骤增,甚至宕机。从而造成一系列的连锁反应,造成零碎解体等状况,这就是缓存雪崩。 解决方案:缓存雪崩的解决方案是将key的过期设置为固定工夫范畴内的一个随机数,让key平均的生效即可。 咱们思考应用redis缓存,因为每次查问的条件都不一样,返回的后果数据又比拟少,咱们思考限度查问都必须有一个固定的查问条件,商家编码。如果查问条件中没有查商家编码,咱们能够通过商家名称,商家业务账号这些条件来反查查商家编码。 这样咱们就能够缓存单个商家编码的所有合同,而后再通过代码应用filter对其余查问条件做反对,防止不同的查问条件都去缓存数据而引发的缓存数据更新,缓存数据淘汰曾经缓存数据统一等问题。 同时只缓存单个商家编码的所有合同,缓存的数据量也是可控,每个缓存的大小也可控,根本不会呈现redis大key的问题。 引入缓存,咱们就要思考缓存数据一致性的问题。 无关缓存一致性问题,可自行百度,这个就不在叙述。 ...

February 1, 2023 · 1 min · jiezi

关于并发:OpenMP-原子指令设计与实现

OpenMP 原子指令设计与实现前言在本篇文章当中次要与大家分享一下 openmp 当中的原子指令 atomic,剖析 #pragma omp atomic 在背地到底做了什么,编译器是如何解决这条指令的。 为什么须要原子指令退出当初有两个线程别离执行在 CPU0 和 CPU1,如果这两个线程都要对同一个共享变量进行更新操作,就会产生竞争条件。如果没有爱护机制来防止这种竞争,可能会导致后果谬误或者程序解体。原子指令就是解决这个问题的一种解决方案,它可能保障操作的原子性,即操作不会被打断或者更改。这样就能保障在多线程环境下更新共享变量的正确性。 比方在上面的图当中,两个线程别离在 CPU0 和 CPU1 执行 data++ 语句,如果目前主存当中的 data = 1 ,而后依照图中的程序去执行,那么主存当中的 data 的最终值等于 2 ,然而这并不是咱们想要的后果,因为有两次加法操作咱们心愿最终在内存当中的 data 的值等于 3 ,那么有什么办法可能保障一个线程在执行 data++ 操作的时候上面的三步操作是原子的嘛(不能够宰割): Load data : 从主存当中将 data 加载到 cpu 的缓存。data++ : 执行 data + 1 操作。Store data : 将 data 的值写回主存。事实上硬件就给咱们提供了这种机制,比方 x86 的 lock 指令,在这里咱们先不去探讨这一点,咱们将在后文当中对此进行认真的剖析。 OpenMP 原子指令在 openmp 当中 #pragma omp atomic 的表达式格局如下所示: #pragma omp atomic表达式;其中表达式能够是一下几种模式: x binop = 表达式;x++;x--;++x;--x;二元运算符 binop 为++, --, +, -, *, /, &, ^, | , >>, <<或 || ,x 是根本数据类型 int,short,long,float 等数据类型。 ...

January 21, 2023 · 4 min · jiezi

关于并发:OpenMP-环境变量使用总结

OpenMP 环境变量应用总结OMP_CANCELLATION,在 OpenMP 标准 4.5 当中规定了勾销机制,咱们能够应用这个环境变量去设置是否启动勾销机制,如果这个值等于 TRUE 那么就是开启线程勾销机制,如果这个值等于 FALSE 那么就是敞开勾销机制。#include <stdio.h>#include <omp.h>int main(){ int s = omp_get_cancellation(); printf("%d\n", s);#pragma omp parallel num_threads(8) default(none) { if (omp_get_thread_num() == 2) {#pragma omp cancel parallel } printf("tid = %d\n", omp_get_thread_num()); } return 0;}在下面的程序当中,如果咱们启动勾销机制,那么线程号等于 2 的线程就不会执行前面的 printf 语句。 ➜ cmake-build-hun git:(master) ✗ export OMP_CANCELLATION=TRUE # 启动勾销机制➜ cmake-build-hun git:(master) ✗ ./cancel 1tid = 0tid = 4tid = 1tid = 3tid = 5tid = 6tid = 7OMP_DISPLAY_ENV,这个环境变量的作用就是程序在执行的时候首先会打印 OpenMP 相干的环境变量。如果这个环境变量值等于 TRUE 就会打印环境变量的值,如果是 FLASE 就不会打印。➜ cmake-build-hun git:(master) ✗ export OMP_DISPLAY_ENV=TRUE ➜ cmake-build-hun git:(master) ✗ ./critical OPENMP DISPLAY ENVIRONMENT BEGIN _OPENMP = '201511' OMP_DYNAMIC = 'FALSE' OMP_NESTED = 'FALSE' OMP_NUM_THREADS = '32' OMP_SCHEDULE = 'DYNAMIC' OMP_PROC_BIND = 'FALSE' OMP_PLACES = '' OMP_STACKSIZE = '0' OMP_WAIT_POLICY = 'PASSIVE' OMP_THREAD_LIMIT = '4294967295' OMP_MAX_ACTIVE_LEVELS = '2147483647' OMP_CANCELLATION = 'TRUE' OMP_DEFAULT_DEVICE = '0' OMP_MAX_TASK_PRIORITY = '0' OMP_DISPLAY_AFFINITY = 'FALSE' OMP_AFFINITY_FORMAT = 'level %L thread %i affinity %A'OPENMP DISPLAY ENVIRONMENT ENDdata = 0OMP_DYNAMIC,如果将这个环境变量设置为true,OpenMP实现能够调整用于执行并行区域的线程数,以优化系统资源的应用。与这个环境变量相干的一共有两个函数:void omp_set_dynamic(int);int omp_get_dynamic(void);omp_set_dynamic 应用这个函数示意是否设置动静调整线程的个数,如果传入的参数不等于 0 示意开始,如果参数等于 0 就示意敞开动静调整。 ...

January 19, 2023 · 4 min · jiezi

关于并发:Openmp-Runtime-库函数汇总上

Openmp Runtime 库函数汇总(上)omp_in_parallel,如果以后线程正在并行域外部,则此函数返回true,否则返回false。#include <stdio.h>#include <omp.h>int main(){ printf("0> Not In parallel region value = %d\n", omp_in_parallel()); // 在这里函数返回的 false 在 C 语言当中返回值等于 int 类型的 0 if (omp_in_parallel()) { printf("1> In parallel region value = %d\n", omp_in_parallel()); } #pragma omp parallel num_threads(2) default(none) { // 这里函数的返回值是 1 在 C 语言当中对应 int 类型的 1 if (omp_in_parallel()) { printf("2> In parallel region value = %d\n", omp_in_parallel()); } } return 0;}下面的程序的输入后果如下所示: 0> Not In parallel region value = 02> In parallel region value = 12> In parallel region value = 1须要留神的是在下面的函数 omp_in_parallel 应用时须要留神,如果你的并行域只有一个线程的时候 omp_in_parallel 返回的是 false。比方上面的例子: ...

January 15, 2023 · 6 min · jiezi

关于并发:Pthread-并发编程三深入理解线程取消机制

Pthread 并发编程(三)——深刻了解线程勾销机制根本介绍线程勾销机制是 pthread 给咱们提供的一种用于勾销线程执行的一种机制,这种机制是在线程外部实现的,仅仅可能在共享内存的多线程程序当中应用。 根本应用#include <stdio.h>#include <pthread.h>#include <assert.h>#include <unistd.h>void* task(void* arg) { usleep(10); printf("step1\n"); printf("step2\n"); printf("step3\n"); return NULL;}int main() { void* res; pthread_t t1; pthread_create(&t1, NULL, task, NULL); int s = pthread_cancel(t1); if(s != 0) // s == 0 mean call successfully fprintf(stderr, "cancel failed\n"); pthread_join(t1, &res); assert(res == PTHREAD_CANCELED); return 0;}下面的程序的输入后果如下: step1在下面的程序当中,咱们应用一个线程去执行函数 task,而后主线程会执行函数 pthread_cancel 去勾销线程的执行,从下面程序的输入后果咱们能够晓得,执行函数 task 的线程并没有执行实现,只打印出了 step1 ,这阐明线程被勾销执行了。 深入分析线程勾销机制在上文的一个例子当中咱们简略的应用了一下线程勾销机制,在本大节当中将深入分析线程的勾销机制。在线程勾销机制当中,如果一个线程被失常勾销执行了,其余线程应用 pthread_join 去获取线程的退出状态的话,线程的退出状态为 PTHREAD_CANCELED 。比方在下面的例子当中,主线程勾销了线程 t1 的执行,而后应用 pthread_join 函数期待线程执行实现,并且应用参数 res 去获取线程的退出状态,在下面的代码当中咱们应用 assert 语句去判断 res 的后果是否等于 PTHREAD_CANCELED ,从程序执行的后果来看,assert 通过了,因而线程的退出状态验证正确。 ...

November 18, 2022 · 5 min · jiezi

关于并发:OpenMP-入门

OpenMP 入门简介OpenMP 一个十分易用的共享内存的并行编程框架,它提供了一些非常简单易用的API,让编程人员从简单的并发编程当中释放出来,专一于具体性能的实现。openmp 次要是通过编译领导语句以及他的动静运行时库实现,在本篇文章当中咱们次要介绍 openmp 一些入门的简略指令的应用。 意识 openmp 的简略易用性比方当初咱们有一个工作,启动四个线程打印 hello world,咱们看看上面 C 应用 pthread 的实现以及 C++ 应用规范库的实现,并比照他们和 openmp 的实现复杂性。 C 语言实现#include <stdio.h>#include <pthread.h>void* func(void* args) { printf("hello world from tid = %ld\n", pthread_self()); return NULL;}int main() { pthread_t threads[4]; for(int i = 0; i < 4; i++) { pthread_create(&threads[i], NULL, func, NULL); } for(int i = 0; i < 4; i++) { pthread_join(threads[i], NULL); } return 0;}下面文件编译命令:gcc 文件名 -lpthread 。 ...

October 30, 2022 · 3 min · jiezi

关于并发:自己动手写乞丐版线程池

本人入手写乞丐版线程池前言在上篇文章线程池的前世今生当中咱们介绍了实现线程池的原理,在这篇文章当中咱们次要介绍实现一个十分简易版的线程池,深刻的去了解其中的原理,麻雀虽小,五脏俱全。 线程池的具体实现线程池实现思路工作保留到哪里?在上一篇文章线程池的前世今生当中咱们具体去介绍了线程池当中的原理。在线程池当中咱们有很多个线程一直的从工作池(用户在应用线程池的时候一直的应用execute办法将工作增加到线程池当中)外面去拿工作而后执行,当初须要思考咱们应该用什么去实现工作池呢? 答案是阻塞队列,因为咱们须要保障在多个线程往工作池外面退出工作的时候并发平安,JDK曾经给咱们提供了这样的数据结构——BlockingQueue,这个是一个并发平安的阻塞队列,他之所以叫做阻塞队列,是因为咱们能够设置队列当中能够包容数据的个数,当退出到队列当中的数据超过这个值的时候,试图将数据退出到阻塞队列当中的线程就会被挂起。当队列当中为空的时候,试图从队列当中取出数据的线程也会被挂起。 线程的设计在咱们本人实现的线程池当中咱们定一个Worker类去一直的从工作池当中取出工作,而后进行执行。在咱们本人定义的worker当中还须要有一个变量isStopped示意线程是否进行工作。同时在worker当中还须要保留以后是哪个线程在执行工作,因而在咱们本人设计的woker类当中还须要有一个thisThread变量,保留正在执行工作的线程,因而worker的整体设计如下: package cscore.concurrent.java.threadpool;import java.util.concurrent.BlockingQueue;public class Worker implements Runnable { private Thread thisThread; // 示意正在执行工作的线程 private BlockingQueue<Runnable> taskQueue; // 由线程池传递过去的工作队列 private volatile boolean isStopped; // 示意 worker 是否进行工作 须要应用 volatile 保障线程之间的可见性 public Worker(BlockingQueue taskQueue) { // 这个构造方法是在线程池的实现当中会被调用 this.taskQueue = taskQueue; } // 线程执行的函数 @Override public void run() { thisThread = Thread.currentThread(); // 获取执行工作的线程 while (!isStopped) { // 当线程没有进行的时候就一直的去工作池当中取出工作 try { Runnable task = taskQueue.take(); // 从工作池当中取出工作 当没有工作的时候线程会被这个办法阻塞 task.run(); // 执行工作 工作就是一个 Runnable 对象 } catch (InterruptedException e) { // do nothing // 这个中央很重要 你有没有思考过一个问题当工作池当中没有工作的时候 线程会被阻塞在 take 办法上 // 如果咱们前面没有工作提交拿他就会始终阻塞 那么咱们该如何唤醒他呢 // 答案就在上面的函数当中 调用线程的 interruput 办法 那么take办法就会产生一个异样 而后咱们 // 捕捉到一异样 而后线程退出 } } } public synchronized void stopWorker() { if (isStopped) { throw new RuntimeException("thread has been interrupted"); } isStopped = true; thisThread.interrupt(); // 中断线程产生异样 } public synchronized boolean isStopped() { return isStopped; }}线程池的参数在咱们本人实现的线程池当中,咱们只须要定义两个参数一个是线程的个数,另外一个是阻塞队列(工作池)当中最大的工作个数。在咱们本人实现的线程池当中还须要有一个变量isStopped示意线程池是否进行工作了,因而线程池的初步设计大抵如下: ...

October 18, 2022 · 3 min · jiezi

关于并发:彻底了解线程池的原理40行从零开始自己写线程池

彻底理解线程池的原理——40行从零开始本人写线程池前言在咱们的日常的编程当中,并发是始终离不开的主题,而在并发多线程当中,线程池又是一个不可躲避的问题。多线程能够进步咱们并发程序的效率,能够让咱们不去频繁的申请和开释线程,这是一个很大的花销,而在线程池当中就不须要去频繁的申请线程,他的次要原理是申请完线程之后并不中断,而是一直的去队列当中支付工作,而后执行,重复这样的操作。在本篇文章当中咱们次要是介绍线程池的原理,因而咱们会本人写一个十分非常简单的线程池,次要帮忙大家了解线程池的外围原理!!! 线程池给咱们提供的性能咱们首先来看一个应用线程池的例子: import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Demo01 { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { pool.execute(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " print " + i); } } }); } }}在下面的例子当中,咱们应用Executors.newFixedThreadPool去生成来一个固定线程数目的线程池,在下面的代码当中咱们是应用5个线程,而后通过execute办法一直的去向线程池当中提交工作,大抵流程如下图所示: 线程池通过execute函数一直的往线程池当中的工作队列退出工作,而线程池当中的线程会一直的从工作队列当中取出工作,而后进行执行,而后持续取工作,继续执行....,线程的执行过程如下: while (true) { Runnable runnable = taskQueue.take(); // 从工作队列当中取出工作 runnable.run(); // 执行工作}依据下面所谈到的内容,当初咱们的需要很清晰了,首先咱们须要有一个队列去存储咱们所须要的工作,而后须要开启多个线程一直的去工作队列当中取出工作,而后进行执行,而后反复取工作执行工作的操作。 ...

August 18, 2022 · 3 min · jiezi

关于并发:30行自己写并发工具类Semaphore-CyclicBarrier-CountDownLatch是什么体验

30行本人写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验?前言在本篇文章当中首先给大家介绍三个工具Semaphore, CyclicBarrier, CountDownLatch该如何应用,而后认真分析这三个工具外部实现的原理,最初会跟大家一起用ReentrantLock实现这三个工具。 并发工具类的应用CountDownLatchCountDownLatch最次要的作用是容许一个或多个线程期待其余线程实现操作。比方咱们当初有一个工作,有$N$个线程会往数组data[N]当中对应的地位依据不同的工作放入数据,在各个线程将数据放入之后,主线程须要将这个数组当中所有的数据进行求和计算,也就是说主线程在各个线程放入之前须要阻塞住!在这样的场景下,咱们就能够应用CountDownLatch。下面问题的代码: import java.util.Arrays;import java.util.Random;import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo { public static int[] data = new int[10]; public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { int temp = i; new Thread(() -> { Random random = new Random(); data[temp] = random.nextInt(100001); latch.countDown(); }).start(); } // 只有函数 latch.countDown() 至多被调用10次 // 主线程才不会被阻塞 // 这个10是在CountDownLatch初始化传递的10 latch.await(); System.out.println("求和后果为:" + Arrays.stream(data).sum()); }}在下面的代码当中,主线程通过调用latch.await();将本人阻塞住,而后须要等他其余线程调用办法latch.countDown()只有这个办法被调用的次数等于在初始化时给CountDownLatch传递的参数时,主线程才会被开释。 ...

July 22, 2022 · 4 min · jiezi

关于并发:并发执行串行

并发执行如何串行?如何串行? 答: 粘接。即: 单个节点是原子性的、失当的粘接多个节点留神: 单节点原子性 由组件实现,而 失当的粘接 由利用提供。 如何串行举例:A. 多个cas指令失当的粘接在一起 能够达到 确定的程序 。这里 失当的粘接是: 操作cas所用的值序列是递增的或递加的 单个cas指令 是原子性的,因为cas是 两条指令合并为一条指令 这条指令就是cas指令 对cpu来说,单条cpu指令是原子性的 是不被打断的,因为cpu总是在一条指令完结时才查看是否有中断要跳转过来。而在一条指令两头是不进行中断查看的 B. zk的一个写操作是原子性的, 用版本号将这些写操作粘接在一起,能够达到 确定的程序,这也即 乐观锁 实现。这里 失当的粘接是: 版本号

December 9, 2021 · 1 min · jiezi

关于并发:IO-模式

I/O模式阻塞 I/O个别设施有两种操作:读和写。其中随同读缓冲区和写缓冲区。 读缓冲区无数据:当读数据的速率超过写数据的速率,可能造成缓冲区无数据可读的状况,此时用户程序如果是阻塞 I/O 模式,那么用户程序将会进入期待,在linux中的具体操作就是从零碎的 "Runable Queue” 中删除该过程,将其退出到期待队列“Wait Queue”。只有当设施通过“中断”告知了内核缓冲区已有数据或者内核轮询发现了缓冲区有数据,内核会唤醒该期待队列上的过程,把他们退出可 “Runable Queue”。写缓冲区满数据:相似,当写数据的速率超过读数据的速率,可能造成缓冲区无闲暇内存可写的状况,这时内核同样的会阻塞写线程直到缓冲区可写。非阻塞 I/O相似下面的状况,当无数据可读可写时,执行 read/write(个别 read/write 办法会返回此次调用读取或写了多少字节) 后,内核可间接返回 0,并不阻塞以后线程,把无数据可读可写的结果交由用户程序本人解决。 那什么时候可读和可写?这种 I/O 框架个别存在一种叫 Selector 选择器的线程,由这个线程去轮询所有的已注册的文件描述符(这包含一般文件,套接字等) 缓冲区,当呈现缓冲区可读或可写时,找出在其下面注册了相应的可读事件或可写事件的回调办法,进行调用。但这种形式在解决大量描述符时成果不是很好,因为为缓冲区的可读可写的判断,须要进入内核态。 用户态 切换到内核态的代价挺大的,尽管他们都是同一个线程,但却须要保留硬件上下文,这是因为用户态的数据段、代码段、栈基址、栈指针 和内核态都不一样,每次切换须要切换这些寄存器值。Epoll在服务器过程中,一个 server 过程可能须要治理成千盈百个 Socket。 他们的可读可写如果是阻塞模式,那将造成灾难性的结果,一个网络差的客户端连贯,足以迁延整个零碎。 如果可读可写是异步模式,大量的 Socket 治理将连累 Selector,让 Selector 线程跑满 CPU Linux 应用了 Epoll 模式,改善了下面的状况。 通过应用一片用户态和内核态共享的内存空间,因为此片空间为共享的,所以用户程序向此内存写数据时不须要陷入内核态。用户程序须要写入什么数据?epoll 在此空间保护了一颗红黑树用来保留 Socket,一个就绪列表来援用就绪的 Socket。用户程序能够将本人的 Socket 增加至红黑树,并向 epoll 注册相干事件(可读、可写、连贯...)的监督函数。当网卡承受到了数据或发送了数据后,向操作系统收回了一个硬中断告诉操作系统,操作系统得悉后,在 Epoll 红黑树中找到对应的 socket ,退出到就绪队列,。Epoll 关注了就绪列表,通过就绪列表中的 Socket 向关怀相干事件的过程发送告诉,至此用户程序能够读取或写入数据了。全程没有阻塞,所有操作都是异步的(异步扭转世界),也没有用户态和内核态的非必要切换。

November 25, 2021 · 1 min · jiezi

关于golang:go-更为安全的使用-syncMap-组件

go 内置了协程平安的 sync 包来不便咱们同步各协程之间的执行状态,应用起来也十分不便。 最近在排查解决一个线下服务的数据同步问题,review 外围代码后,发现这么一段流程控制代码。 谬误示例 package mainimport ( "log" "runtime" "sync")func main() { // 可并行也是重点,生产场景没几个单核的吧?? runtime.GOMAXPROCS(runtime.NumCPU()) waitGrp := &sync.WaitGroup{} waitGrp.Add(1) syncTaskProcessMap := &sync.Map{} for i := 0; i < 100; i++ { syncTaskProcessMap.Store(i, i) } for j := 0; j < 100; j++ { go func(j int) { // 协程可能并行抢占一轮开始 syncTaskProcessMap.Delete(j) // 协程可能并行抢占一轮完结 // 在以后协程 Delete 后 Range 前 又被其余协程 Delete 操作了 syncTaskProcessCount := 0 syncTaskProcessMap.Range(func(key, value interface{}) bool { syncTaskProcessCount++ return true }) if syncTaskProcessCount == 0 { log.Println(GetGoroutineID(), "syncTaskProcessMap empty, start syncOnline", syncTaskProcessCount) } }(j) } waitGrp.Wait()}func GetGoroutineID() uint64 { b := make([]byte, 64) runtime.Stack(b, false) b = bytes.TrimPrefix(b, []byte("goroutine ")) b = b[:bytes.IndexByte(b, ' ')] n, _ := strconv.ParseUint(string(b), 10, 64) return n}代码的本意,是在 i 个协程并发的执行实现后,启动一次 nextProcess 工作,代码应用了 sync.Map 来保护和同步 i 个协程的执行进度,避免多协程并发造成的 map 不平安读写。当最初一个协程执行结束,sync.Map 为空,启动一次 nextProcess。但能读到状态值 syncTaskProcessCount 为 0 的协程,只会是 最初一个 执行实现的协程吗? ...

April 20, 2021 · 3 min · jiezi

关于threadlocal:ThreadLocal与ForkJoin使用踩坑记录

前言因为我的项目框架起因,在审慎思考后应用了ThreadLocal存储了上下文。上线一段时间后,发现有些时候拿到的上下文并不是本人线程的上下文,最初定位到是因为应用了java8的并行流,并且在并行流外面拿了一次ThreadLocal的上下文值,剖析了一波起因后,将并行流改为了串行流(或应用ThreadPoolExecutor),状况得以失常。 事件起因因为我的项目构造起因,每次申请过去,咱们零碎都须要申请另外一个零碎获取用户身份信息(dubbo外部调用,不可应用token之类存储)。所以咱们做了一个切面,应用注解的模式在进入此代理对象的办法时,在切面调用另一个零碎,并把用户信息封装好,放入ThreadLocal外面,前面办法外面能够间接在ThreadLocal外面获取,切面完结的时候会在finnaly外面移除ThreadLocal的值。 切面: @Aspect@Component@Slf4jpublic class InterfaceMonitorAspect { //调用三方零碎的Repository @Resource private BountyRepository bountyRepository; @Pointcut("@annotation(com.shizhuang.duapp.finance.loan.center.application.aop.InterfaceMonitor)") public void doMonitor() { log.info("doMonitor开始..."); } @Around("doMonitor()") public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable { ... try{ ... //切面外面调用三方接口 AccountEntity accountEntity = bountyRepository.queryAccountInfo(userId, bizIdentity); //切面外面设置进Threadlocal AccountContextHolder.setAccountEntity(accountEntity); Object retVal = joinPoint.proceed(); ... }catch (Throwable t) { ... }finally{ //移除 AccountContextHolder.clear(); } }}上下文AccountContextHolder /** * create by liuliang * on 2020/12/30 4:46 PM */ public class AccountContextHolder { //可被继承的ThreadLocal private static final ThreadLocal<AccountEntity> contextHolder = new InheritableThreadLocal<>(); /** * 设置 accountEntity * * @param accountEntity */ public static void setAccountEntity(AccountEntity accountEntity) { contextHolder.set(accountEntity); } /** * 获得 accountEntity * * @return */ public static AccountEntity getAccountEntity() { return contextHolder.get(); } /** * 革除上下文数据 */ public static void clear() { contextHolder.remove(); }}问题所在地: ...

April 9, 2021 · 1 min · jiezi

关于并发:go语言happensbefore原则及应用

理解go中happens-before规定,寻找并发程序不确定性中的确定性。 引言先抛开你所熟知的信号量、锁、同步原语等技术,思考这个问题:如何保障并发读写的准确性?一个没有任何并发编程教训的程序员可能会感觉很简略:这有什么问题呢,同时读写能有什么问题,最多就是读到过期的数据而已。一个现实的世界当然是这样,只惋惜实际上的机器世界往往暗藏了很多不容易被觉察的事件。至多有两个行为会影响这个论断: 编译器往往有指令重排序的优化;例如程序员看到的源代码是a=3; b=4;,而实际上执行的程序可能是b=4; a=3;,这是因为编译器为了优化执行效率可能对指令进行重排序;高级编程语言所反对的运算往往不是原子化的;例如a += 3实际上蕴含了读变量、加运算和写变量三次原子操作。既然整个过程并不是原子化的,就意味着随时有其它“入侵者”侵入批改数据。更为暗藏的例子:对于变量的读写甚至可能都不是原子化的。不同机器读写变量的过程可能是不同的,有些机器可能是64位数据一次性读写,而有些机器是32位数据一次读写。这就意味着一个64位的数据在后者的读写上实际上是分成两次实现的!试想,如果你试图读取一个64位数据的值,先读取了低32的数据,这时另一个线程切进来批改了整个数据的值,最初你再读取高32的值,将高32和低32的数据拼成残缺的值,很显著会失去一个预期以外的数据。看起来,整个并发编程的世界里一切都是不确定的,咱们不晓得每次读取的变量到底是不是及时、精确的数据。侥幸的是,很多语言都有一个happens-before的规定,能帮忙咱们在不确定的并发世界里寻找一丝确定性。 happens-before你能够把happens-before看作一种非凡的比拟运算,就如同>、<一样。对应的,还有happens-after,它们之间的关系也如同>、<一样: 如果a happens-before b,那么b happens-after a那是否存在既不满足a happens-before b,也不满足b happens-before a的状况呢,就如同既不满足a>b,也不满足b>a(意味着b==a)?当然是必定的,这种状况称为:a和b happen concurrently,也就是同时产生,这就回到咱们之前所熟知的世界里了。 happens-before有什么用呢?它能够用来帮忙咱们厘清两个并发读写之间的关系。对于并发读写问题,咱们最关怀的常常是reader是否能精确察看到writer写入的值。happens-before正是为这个问题设计的,具体来说,要想让某次读取r精确察看到某次写入w,只需满足: w happens-before r;对变量的其它写入w1,要么w1 happens-before w,要么r happens-before w1;简略了解就是没有其它写入笼罩这次写入;只有满足这两个条件,那咱们就能够自信地必定咱们肯定能读取到正确的值。 一个新的问题随之诞生:那如何判断a happens-before b是否成立呢?你能够类比思考数学里如何判断a > b是否成立的过程,咱们的做法很简略: 基于一些简略的公理;例如自然数的天然大小:3>2>1基于比拟运算符的传递性,也就是如果a>b且b>c,则a>c判断a happens-before b的过程也是相似的:依据一些简略的明确的happens-before关系,再联合happens-before的传递性,推导出咱们所关怀的w和r之间的happens-before关系。 happens-before传递性:如果a happens-before b,且b happens-before c,则a happens-before c因而咱们只须要理解这些明确的happens-before关系,就能在并发世界里寻找到贵重的确定性了。 go语言中的happens-before关系具体的happens-before关系是因语言而异的,这里只介绍go语言相干的规定,感兴趣能够间接浏览官网文档,有更残缺、精确的阐明。 天然执行首先,最简略也是最直观的happens-before规定: 在同一个goroutine里,书写在前的代码happens-before书写在后的代码。例如: a = 3; // (1)b = 4; // (2)则(1) happens-before (2)。咱们下面提到指令重排序,也就是理论执行的程序与书写的程序可能不统一,但happens-before与指令重排序并不矛盾,即便可能产生指令重排序,咱们仍然能够说(1) happens-before (2)。 初始化每个go文件都能够有一个init办法,用于执行某些初始化逻辑。当咱们开始执行某个main办法时,go会先在一个goroutine里做初始化工作,也就是执行所有go文件的init办法,这个过程中go可能创立多个goroutine并发地执行,因而通常状况下各个init办法是没有happens-before关系的。对于init办法有两条happens-before规定: 1.a 包导入了 b包,此时b包的init办法happens-before a包的所有代码;2.所有init办法happens-before main办法;goroutinegoroutine相干的规定次要是其创立和销毁的: 1.goroutine的创立 happens-before 其执行;2.goroutine的实现不保障happens-before任何代码;第一条规定举个简略的例子即可: var a stringfunc f() { fmt.Println(a) // (1)}func hello() { a = "hello, world" // (2) go f() // (3)}因为goroutine的创立 happens-before 其执行,所以(3) happens-before (1),又因为天然执行的规定(2) happens-before (3),依据传递性,所以(2) happens-before (1),这样保障了咱们每次打印进去的都是"hello world"而不是空字符串。 ...

March 28, 2021 · 3 min · jiezi

关于并发:干货分享丨从MPG-线程模型探讨Go语言的并发程序

摘要:Go 语言的并发个性是其一大亮点,明天咱们来带着大家一起看看如何应用 Go 更好地开发并发程序。咱们都晓得计算机的外围为 CPU,它是计算机的运算和管制外围,承载了所有的计算工作。最近半个世纪以来,因为半导体技术的高速倒退,集成电路中晶体管的数量也在大幅度增长,这大大晋升了 CPU 的性能。驰名的摩尔定律——“集成电路芯片上所集成的电路的数目,每隔18个月就翻一番”,形容的就是该种情景。 过于密集的晶体管尽管进步了 CPU 的解决性能,但也带来了单个芯片发热过高和老本过高的问题,与此同时,受限于资料技术的倒退,芯片中晶体管数量密度的减少速度曾经放缓。也就是说,程序曾经无奈简略地依赖硬件的晋升而晋升运行速度。这时,多核 CPU 的呈现让咱们看到了晋升程序运行速度的另一个方向:将程序的执行过程分为多个可并行或并发执行的步骤,让它们别离在不同的 CPU 外围中同时执行,最初将各局部的执行后果进行合并失去最终后果。 并行和并发是计算机程序执行的常见概念,它们的区别在于: · 并行,指两个或多个程序在同一个时刻执行; · 并发,指两个或多个程序在同一个时间段内执行。 并行执行的程序,无论从宏观还是宏观的角度观察,同一时刻内都有多个程序在 CPU 中执行。这就要求 CPU 提供多核计算能力,多个程序被调配到 CPU 的不同的核中被同时执行。 而并发执行的程序,仅须要在宏观角度观察到多个程序在 CPU 中同时执行。即便是单核 CPU 也能够通过分时复用的形式,给多个程序调配肯定的执行工夫片,让它们在 CPU 上被疾速轮换执行,从而在宏观上模拟出多个程序同时执行的成果。但从宏观角度来看,这些程序其实是在 CPU 中被串行执行。 Go 的 MPG 线程模型Go 被认为是一门高性能并发语言,得益于它在原生态反对协程并发。这里咱们首先理解过程、线程和协程这三者的分割和区别。 在多道程序零碎中,过程是一个具备独立性能的程序对于某个数据汇合的一次动静执行过程,是操作系统进行资源分配和调度的根本单位,是利用程序运行的载体。 而线程则是程序执行过程中一个繁多的顺序控制流程,是 CPU 调度和分派的根本单位。线程是比过程更小的独立运行根本单位,一个过程中能够领有一个或者以上的线程,这些线程共享过程所持有的资源,在 CPU 中被调度执行,共同完成过程的执行工作。 在 Linux 零碎中,依据资源拜访权限的不同,操作系统会把内存空间分为内核空间和用户空间:内核空间的代码可能间接拜访计算机的底层资源,如 CPU 资源、I/O 资源等,为用户空间的代码提供计算机底层资源拜访能力;用户空间为下层应用程序的流动空间,无奈间接拜访计算机底层资源,须要借助“零碎调用”“库函数”等形式调用内核空间提供的资源。 同样,线程也能够分为内核线程和用户线程。内核线程由操作系统治理和调度,是内核调度实体,它可能间接操作计算机底层资源,能够充分利用 CPU 多核并行计算的劣势,然而线程切换时须要 CPU 切换到内核态,存在肯定的开销,可创立的线程数量也受到操作系统的限度。用户线程由用户空间的代码创立、治理和调度,无奈被操作系统感知。用户线程的数据保留在用户空间中,切换时毋庸切换到内核态,切换开销小且高效,可创立的线程数量实践上只与内存大小相干。 协程是一种用户线程,属于轻量级线程。协程的调度,齐全由用户空间的代码管制;协程领有本人的寄存器上下文和栈,并存储在用户空间;协程切换时毋庸切换到内核态拜访内核空间,切换速度极快。但这也给开发人员带来较大的技术挑战:开发人员须要在用户空间解决协程切换时上下文信息的保留和复原、栈空间大小的治理等问题。 Go 是为数不多在语言档次实现协程并发的语言,它采纳了一种非凡的两级线程模型:MPG 线程模型(如下图)。 MPG 线程模型 · M,即 machine,相当于内核线程在 Go 过程中的映射,它与内核线程一一对应,代表真正执行计算的资源。在 M 的生命周期内,它只会与一个内核线程关联。 ...

March 9, 2021 · 4 min · jiezi

关于并发:说说Golang-goroutine并发那些事儿

摘要:明天咱们一起盘点一下Golang并发那些事儿。Golang、Golang、Golang 真的够浪,明天咱们一起盘点一下Golang并发那些事儿,精确来说是goroutine,对于多线程并发,咱们临时先放一放(次要是俺当初还不太会,不敢进去瞎搞)。对于golang长处如何,咱们也不扯那些虚的。反正都是大佬在说,俺只是个吃瓜大众,偶然打打酱油,逃~。 说到并发,等等一系列的概念就进去了,为了做个关照一下本人的菜,顺便温习一下 根底概念过程过程的定义过程(英语:process),是指计算机中已运行的程序。过程已经是 ` `分时系统的根本运作单位。在面向过程设计的零碎(如晚期的UNIX,Linux 2.4及更早的版本)中,过程是程序的根本执行实体;在面向线程设计的零碎(如当代少数操作系统、Linux` 2.6及更新的版本)中,过程自身不是根本运行单位,而是线程的容器。 程序自身只是指令、数据及其组织模式的形容,相当于一个名词,过程才是程序(那些指令和数据)的真正运行实例,能够想像说是当初进行式。若干过程有可能与同一个程序相关系,且每个过程皆能够同步或异步的形式独立运行。古代计算机系统可在同一段时间内以过程的模式将多个程序加载到存储器中,并借由工夫共享(或称时分复用),以在一个处理器上体现出同时平行性运行的感觉。同样的,应用多线程技术(多线程即每一个线程都代表一个过程内的一个独立执行上下文)的操作系统或计算机体系结构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。 过程的创立操作系统须要有一种形式来创立过程。 以下4种次要事件会创立过程 零碎初始化 (简略可了解为关机后的开机)正在运行的程序执行了创立过程的零碎调用(例如:敌人发了一个网址,你点击后开启浏览器进入网页中)用户申请创立一个新过程(例如:关上一个程序,关上QQ、微信)一个批量作业的初始化过程的终止过程在创立后,开始运行与解决相干工作。但并不会永恒存在,终究会实现或退出。那么以下四种状况会产生过程的终止 失常退出(被迫)谬误退出(被迫)解体退出(非被迫)被其余杀死(非被迫)失常退出:你退出浏览器,你点了一下它 谬误退出:你此时正在津津乐道的看着电视剧,忽然程序外部产生bug,导致退出 解体退出:你程序解体了 被其余杀死:例如在windows上,应用工作管理器敞开过程 过程的状态运行态(理论占用CPU)就绪态(可运行、但其余过程正在运行而暂停)阻塞态(除非某种内部的工夫产生,否则过程不能运行)前两种状态在逻辑上是相似的。处于这两种状态的过程都能够运行,只是对于第二种状态临时没有调配CPU,一旦调配到了CPU即可运行 第三种状态与前两种不同,处于该状态的过程不能运行,即是CPU闲暇也不行。 如有趣味,可进一步理解过程的实现、多过程设计模型 过程池过程池技术的利用至多由以下两局部组成: 资源过程 事后创立好的闲暇过程,治理过程会把工作散发到闲暇过程来解决。 治理过程 治理过程负责创立资源过程,把工作交给闲暇资源过程解决,回收曾经解决完工作的资源过程。 资源过程跟治理过程的概念很好了解,治理过程如何无效的治理资源过程,分配任务给资源过程,回收闲暇资源过程,治理过程要无效的治理资源过程,那么治理过程跟资源过程间必然须要交互,通过IPC,信号,信号量,音讯队列,管道等进行交互。 过程池:精确来说它并不理论存在于咱们的操作系统中,而是IPC,信号,信号量,音讯队列,管道等对多过程进行治理,从而缩小一直的开启、敞开等操作。以求达到缩小不必要的资源损耗 线程定义线程(英语:thread)是操作系统可能进行运算调度的最小单位。大部分状况下,它被蕴含在过程之中,是过程中的理论运作单位。一条线程指的是过程中一个繁多程序的控制流,一个过程中能够并发多个线程,每条线程并行执行不同的工作。在Unix System V及SunOS中也被称为轻量过程(lightweight processes),但轻量过程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。 线程是独立调度和分派的根本单位。线程能够为操作系统内核调度的内核线程 同一过程中的多条线程将共享该过程中的全副系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一过程中的多个线程有各自的调用栈(call stack),本人的寄存器环境(register context),本人的线程本地存储(thread-local storage)。 一个过程能够有很多线程来解决,每条线程并行执行不同的工作。如果过程要实现的工作很多,这样需很多线程,也要调用很多外围,在多核或多CPU,或反对Hyper-threading的CPU上应用多线程程序设计的益处是不言而喻的,即进步了程序的执行吞吐率。以人工作的样子想像,外围相当于人,人越多则能同时解决的事件越多,而线程相当于手,手越多则工作效率越高。在单CPU单核的计算机上,应用多线程技术,也能够把过程中负责I/O解决、人机交互而常被阻塞的局部与密集计算的局部离开来执行,编写专门的workhorse线程执行密集计算,尽管多任务比不上多核,但因为具备多线程的能力,从而进步了程序的执行效率。 线程池线程池(英语:thread pool):一种线程应用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池保护着多个线程,期待着监督管理者调配可并发执行的工作。这防止了在解决短时间工作时创立与销毁线程的代价。线程池不仅可能保障内核的充分利用,还能避免过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数个别取cpu数量+2比拟适合,线程数过多会导致额定的线程切换开销。 任务调度以执行线程的常见办法是应用同步队列,称作工作队列。池中的线程期待队列中的工作,并把执行完的工作放入实现队列中。 线程池模式个别分为两种:HS/HA半同步/半异步模式、L/F领导者与跟随者模式。 · 半同步/半异步模式又称为生产者消费者模式,是比拟常见的实现形式,比较简单。分为同步层、队列层、异步层三层。同步层的主线程解决工作工作并存入工作队列,工作线程从工作队列取出工作进行解决,如果工作队列为空,则取不到工作的工作线程进入挂起状态。因为线程间有数据通信,因而不适于大数据量替换的场合。 · 领导者跟随者模式,在线程池中的线程可处在3种状态之一:领导者leader、追随者follower或工作者processor。任何时刻线程池只有一个领导者线程。事件达到时,领导者线程负责音讯拆散,并从处于追随者线程中选出一个来当继任领导者,而后将本身设置为工作者状态去处理该事件。处理完毕后工作者线程将本身的状态置为追随者。这一模式实现简单,但防止了线程间替换工作数据,进步了CPU cache相似性。在ACE(Adaptive Communication Environment)中,提供了领导者跟随者模式实现。 线程池的伸缩性对性能有较大的影响。 创立太多线程,将会节约肯定的资源,有些线程未被充沛应用。销毁太多线程,将导致之后浪费时间再次创立它们。创立线程太慢,将会导致长时间的期待,性能变差。销毁线程太慢,导致其它线程资源饥饿。协程协程,英文叫作 Coroutine,又称微线程、纤程,协程是一种用户态的轻量级线程。 协程领有本人的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保留到其余中央,在切回来的时候,复原先前保留的寄存器上下文和栈。因而协程能保留上一次调用时的状态,即所有部分状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。 协程实质上是个单过程,协程绝对于多过程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。 串行多个工作,执行结束后再执行另一个。 例如:吃完饭后漫步(先坐下吃饭、吃完后去漫步) 并行多个工作、交替执行 例如:做饭,一会放水洗菜、一会排汇(菜比拟脏,洗下菜写下手,傲娇~) 并发独特登程 边吃饭、边看电视 阻塞与非阻塞阻塞阻塞状态指程序未失去所需计算资源时被挂起的状态。程序在期待某个操作实现期间,本身无奈持续解决其余的事件,则称该程序在该操作上是阻塞的。 常见的阻塞模式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输出阻塞等。阻塞是无处不在的,包含 CPU 切换上下文时,所有的过程都无奈真正解决事件,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。 ...

February 9, 2021 · 1 min · jiezi

关于并发:近万字图文并茂详解AQS加锁流程

靓仔靓女们好,咱们又见面了,我是公众号:java小杰要加油,现就职于京东,致力于分享java相干常识,包含但不限于并发、多线程、锁、mysql以及京东面试真题AQS介绍AQS全称是AbstractQueuedSynchronizer,是一个形象队列同步器,JUC并发包中的大部分的并发工具类,都是基于AQS实现的,所以了解了AQS就算是四舍五入把握了JUC了(好一个四舍五入学习法)那么AQS到底有什么神奇之处呢?有什么特点呢?让咱们明天就来拔光它,一探到底! state:代表被抢占的锁的状态队列:没有抢到锁的线程会包装成一个node节点寄存到一个双向链表中AQS大略长这样,如图所示: 你说我轻易画的,我可不是轻易画的啊,我是有bear而来,来看下AQS根本属性的代码 那么这个Node节点又蕴含什么呢?来吧,展现。 那么咱们就能够把这个队列变的更具体一点 怎么忽然进去个exclusiveOwnerThread?还是保留以后取得锁的线程,哪里来的呢还记得咱们AQS一开始继承了一个类吗 这个exclusiveOwnerThread就是它外面的属性 再次回顾总结一下,AQS属性如下: state:代表被抢占的锁的状态exclusiveOwnerThread:以后取得锁的线程队列:没有抢到锁的线程会包装成一个node节点寄存到一个双向链表中 Node节点 : * thread: 以后node节点包装的线程 * waitStatus:以后节点的状态 * pre: 以后节点的前驱节点 * next: 以后节点的后继节点 * nextWaiter:示意以后节点对锁的模式,独占锁的话就是null,共享锁为Node() 好了,咱们对AQS大略是什么货色什么构造长什么样子有了个分明的认知,上面咱们间接上硬菜,从源码角度剖析下,AQS加锁,它这个构造到底是怎么变动的呢? 注:以下剖析的都是独占模式下的加锁 独占模式 : 锁只容许一个线程取得 NODE.EXCLUSIVE共享模式 :锁容许多个线程取得 NODE.SHAREDAQS加锁源码——acquire public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }乍一看这是什么啊,没关系,咱们能够把它画成流程图不便咱们了解,流程图如下 上面咱们来一个一个剖析,图文并茂,来吧宝贝儿。 AQS加锁源码——tryAcquire protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }这是什么状况?怎么间接抛出了异样?其实这是由AQS子类重写的办法,就相似lock锁,由子类定义尝试获取锁的具体逻辑 咱们平时应用lock锁时往往如下 (若不想看lock锁怎么实现的能够间接跳转到下一节) ReentrantLock lock = new ReentrantLock(); lock.lock(); try{ //todo }finally { lock.unlock(); }咱们看下lock.lock()源码 ...

January 26, 2021 · 4 min · jiezi

关于并发:java-8-Streams

明天要讲的Stream指的是java.util.stream包中的诸多类。Stream能够不便的将之前的联合类以转换为Stream并以流式形式进行解决,大大的简化了咱们的编程,Stream包中,最外围的就是interface Stream<T> 从下面的图中咱们能够看到Stream继承自BaseStream。Stream中定义了很多十分实用的办法,比方filter,map,flatmap,forEach,reduce,collect等等。接下来咱们将会逐个解说。 创立StreamStream的创立有很多形式,java引入Stream之后所有的汇合类都增加了一个stream()办法,通过这个办法能够间接失去其对应的Stream。也能够通过Stream.of办法来创立: //Stream Creation String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); stream = Stream.of("a", "b", "c");Streams多线程如果咱们想应用多线程来解决汇合类的数据,Stream提供了十分不便的多线程办法parallelStream(): //Multi-threading List<String> list =new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("abc"); list.add("ccc"); list.add("ddd"); list.parallelStream().forEach(element -> doPrint(element));Stream的基本操作Stream的操作能够分为两类,一类是两头操作,两头操作返回Stream<T>,因而能够级联调用。 另一类是终止操作,这类操作会返回Stream定义的类型。 //Operations long count = list.stream().distinct().count();下面的例子中,distinct()返回一个Stream,所以能够级联操作,最初的count()是一个终止操作,返回最初的值。 MatchingStream提供了anyMatch(), allMatch(), noneMatch()这三种match形式,咱们看下怎么应用: //Matching boolean isValid = list.stream().anyMatch(element -> element.contains("h")); boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); Filteringfilter() 办法容许咱们对Stream中的数据进行过滤,从而失去咱们须要的: Stream<String> filterStream = list.stream().filter(element -> element.contains("d"));下面的例子中咱们从list中选出了蕴含“d”字母的String。 ...

January 11, 2021 · 1 min · jiezi

关于并发:七周七并发模型

并发在当初曾经是非常常见的问题了,因为人类信息量的减少,很多信息都须要并发解决,原有的串行解决曾经很难满足事实的需要。 当今支流语言都竞相反对不同的并发模型,例如CSP模型、数据并行、函数式编程和Clojure的unified succession model。 基于锁和线程的并发模型是目前最罕用的一种并发模型,然而并发编程模型不仅仅只有这一种。 理解和相熟各种并发编程模型,在解决并发问题时会有更多思路。 最近很多小伙伴问我要一些 并发模型 相干的材料,于是我翻箱倒柜,找到了这本十分经典的电子书——《七周七并发模型》。 材料介绍《七周七并发模型》从最根底的线程和锁模型讲起,简直涵盖了目前所有的并发编程模型。本书通过以下七个精选的模型帮忙读者理解并发畛域的轮廓:线程与锁,函数式编程,Clojure,actor,通信顺序进程,数据级并行,Lambda架构。书中每一章都设计成三天的浏览量,每天浏览完结都会有相干练习,适宜各类想学习并发模型的人群。 如何获取?1.辨认二维码并关注公众号「Java后端技术全栈」; 2.在公众号后盾回复关键字「942」。

December 27, 2020 · 1 min · jiezi

关于并发:Java-并发编程AQS-的原子性如何保证

当咱们钻研AQS框架时(对于AQS不太熟知能够先浏览《什么是JDK内置并发框架AQS》,会发现AbstractQueuedSynchronizer这个类很多中央都应用了CAS操作。在并发实现中CAS操作必须具备原子性,而且是硬件级别的原子性。咱们晓得Java被隔离在硬件之上,硬件级别的操作显著力不从心。这时为了可能执行操作系统层面的操作,就必须要通过用C++编写的native本地办法来扩大实现。个别能够通过JNI形式实现Java代码调用C++代码 Unsafe调用JDK提供了一个类来满足CAS的硬件级别原子性要求,即sun.misc.Unsafe类,从名字上大略晓得它用于执行低级别、不平安的操作,AQS就是应用此类来实现硬件级别的原子操作。也就是说通过该类就能实现对处理器的原子操作,Unsafe通过JNI调用本地C++代码,C++代码调用了硬件指令集,这些硬件指令集都属于CPU。 Unsafe的魔法Unsafe是一个很弱小的类,它能够分配内存、开释内存、能够定位对象某字段的地位、能够批改对象的字段值、能够使线程挂起、使线程复原、可进行硬件级别原子的CAS操作等等。 image.png Unsafe的用处因为存在安全性问题,所以如果咱们要用Unsafe类则须要另辟蹊径。可行的办法就是通过反射来绕过上述的安全检查,咱们能够通过以下的getUnsafeInstance办法来获取Unsafe实例。这段代码演示了如何获取Java对象的绝对地址偏移量以及应用Unsafe来实现CAS操作,最终输入的是flag字段的内存偏移量及CAS操作后的值。最终的输入为“unsafeTest对象的flag字段的地址偏移量为:12”和“CAS操作后的flag值为:101”。另外如果应用开发工具如Eclipse,可能会编译通不过,只有把编译谬误提醒关掉即可。 Unsafe实现CAS因为存在安全性问题,所以如果咱们要用Unsafe类则须要另辟蹊径。可行的办法就是通过反射来绕过上述的安全检查,咱们能够通过以下的getUnsafeInstance办法来获取Unsafe实例。这段代码演示了如何获取Java对象的绝对地址偏移量以及应用Unsafe来实现CAS操作,最终输入的是flag字段的内存偏移量及CAS操作后的值。最终的输入为“unsafeTest对象的flag字段的地址偏移量为:12”和“CAS操作后的flag值为:101”。另外如果应用开发工具如Eclipse,可能会编译通不过,只有把编译谬误提醒关掉即可。 总结这里次要解说了Unsafe类如何让Java层能实现硬件级别的原子操作,同时也理解了Unsafe类领有很多法魔技能。通常咱们应用Java时不须要在内存中解决Java对象及内存地址地位,但有的时候咱们被迫必须要操作Java对象相干的地址,于是咱们只能应用Unsafe类。应用该类则意味着毁坏了Java平台隔离的成果了,咱们都晓得一旦用了本中央法令可能会引来跨平台问题。 Java 并发编程Java并发编程:AQS的原子性如何保障Java并发编程:如何避免在线程阻塞与唤醒时死锁Java并发编程:多线程如何实现阻塞与唤醒Java并发编程:工作执行器Executor接口Java并发编程:并发中死锁的造成条件及解决Java并发编程:Java 序列化的工作机制Java并发编程:过程、线程、并行与并发

December 21, 2020 · 1 min · jiezi

关于并发:高并发账户记录查询

【摘要】面对高并发账户记录查问问题,依照本文的介绍一步一步操作,就能无效晋升性能。点击理解高并发账户记录查问 问题形容高并发账户记录查问在银行、互联网企业、通信企业中宽泛存在。例如:网上银行、手机银行、电商个人账户查问、互联网游戏账户等等。这类查问有三个共同点: 1、 数据总量十分大。用户数量自身就十分多,再加上多年的账户数据,数据量能够达到几千万甚至上亿条。 2、 拜访人数泛滥。几百万甚至上千万人拜访,属于高并发查问。 3、 不能让用户期待。手机、网页要达到秒级响应,否则重大影响用户体验。 上面以某银行账户定期明细查问为例,给出这类问题的解决办法。 某银行共一亿个定期账户,每个账户均匀每月有 7 条数据,每年数据总量 84 亿条。每条数据中的机构字段,还要关联分支机构表(几千条)记录。在性能上,要求单台服务器反对一千个以上的查问,响应工夫不能超过 1 秒。 有序行存定期明细数据随着工夫增长十分快,一年就有 84 亿条。如果放到内存中,须要大量内存空间,硬件投入老本太高,所以要放到硬盘上存储。分支机构表只有几千条数据,能够放在内存中存储。 在硬盘上存储,要思考是行存还是列存。列存数据分块压缩,能缩小遍历数据量。但因为账户查问是随机的,整块读取会有额定解压计算。而且每次取数都针对整个分块,复杂度较高,性能不如行存。因而,这个场景要抉择行存存储,如下图: 图 1:行存和列存 具体的实现能够采纳 Java、C++、SPL 等高级语言。这里咱们以代码量起码的 SPL 语言为例解说。 代码示例 1 A1:连贯生产数据库,用游标读取活期存款数据,依照账户 id 排序。 A2:建设本地组表文件。 A3:建设组表,并从数据库游标读取活期存款明细数据,写入文件。 其中,A3 中的 @r 选项,就是建设行存文件。一年 84 亿条数据都导出,工夫会比拟长。然而这是一次性的工作,后续就只须要追加增量数据即可。增量数据的追加办法,前面会有介绍。如果依照账户排序会对生产数据库造成较大压力,能够导出之后基于文件排序。排序应用 SPL 的 sortx 函数,具体用法参见函数参考。 利用索引要利用索引提速,先要对明细文件建设索引。因为明细数据量大,建设的索引文件也会很大。很难全副加载到内存中。能够建设多级索引,如下图: 图 2:多级缓存 还是以 SPL 为例,建设多级索引,只须要在“代码示例 1”的根底上,减少一个网格即可: 代码示例 2 A4:对行存文件建设索引文件。 加载的索引级别越多,占用存储空间越大。同时,账户 id 的跨度变小,加载到内存中后,索引成果也会变好。查问时能够依据内存大小,尽可能加载更多级别的索引,能够无效进步查问速度。 在查问之前,零碎初始化或者数据变动时,要事后加载多级索引,以 SPL 代码为例: 代码示例 3 A1:判断全局变量中是否存在 detailR,如果存在,示意曾经加载了索引。 ...

December 17, 2020 · 1 min · jiezi

关于并发:前端面试每日-31-第610天

明天的知识点 (2020.12.16) —— 第610天 (我也要出题)[html] 写一个布局,它的宽度是不固定的100%,如果让它的宽度始终是高度的一半呢?[css] Sass中的@media指令有什么作用?[js] 解释下JavaScript并发模型[软技能] 请说说你对用以至学的了解?它和学以致用有什么不同?《论语》,曾子曰:“吾日三省吾身”(我每天屡次检查本人)。前端面试每日3+1题,以面试题来驱动学习,每天提高一点!让致力成为一种习惯,让奋斗成为一种享受!置信 保持 的力量!!!欢送在 Issues 和敌人们一起探讨学习! 我的项目地址:前端面试每日3+1【举荐】欢送跟 jsliang 一起折腾前端,零碎整顿前端常识,目前正在折腾 LeetCode,打算买通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个Star, 同时欢送微信扫码关注 前端剑解 公众号,并退出 “前端学习每日3+1” 微信群互相交换(点击公众号的菜单:交换)。 学习不打烊,充电加油只为遇到更好的本人,365天无节假日,每天早上5点纯手工公布面试题(死磕本人,愉悦大家)。心愿大家在这虚夸的前端圈里,放弃沉着,保持每天花20分钟来学习与思考。在这变幻无穷,类库层出不穷的前端,倡议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢送大家到Issues交换,激励PR,感激Star,大家有啥好的倡议能够加我微信一起交换探讨!心愿大家每日去学习与思考,这才达到来这里的目标!!!(不要为了谁而来,要为本人而来!)交换探讨欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个[Star]

December 16, 2020 · 1 min · jiezi

关于并发:AQS-自定义同步锁挺难的

AQS是AbstractQueuedSynchronizer的简称。 AbstractQueuedSynchronizer 同步状态AbstractQueuedSynchronizer 外部有一个state属性,用于批示同步的状态: private volatile int state;state的字段是个int型的,它的值在AbstractQueuedSynchronizer中是没有具体的定义的,只有子类继承AbstractQueuedSynchronizer那么state才有意义,如在ReentrantLock中,state=0示意资源未被锁住,而state>=1的时候,示意此资源曾经被另外一个线程锁住。 AbstractQueuedSynchronizer中尽管没有具体获取、批改state的值,然而它为子类提供一些操作state的模板办法: 获取状态 protected final int getState() { return state; }更新状态 protected final void setState(int newState) { state = newState; }CAS更新状态 protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }AQS 期待队列AQS 期待列队是一个双向队列,队列中的成员都有一个prev和next成员,别离指向它后面的节点和前面的节点。 队列节点在AbstractQueuedSynchronizer外部,期待队列节点由外部动态类Node示意: static final class Node { ...}节点模式队列中的节点有两种模式: 独占节点:同一时刻只能有一个线程拜访资源,如ReentrantLock共享节点:同一时刻容许多个线程拜访资源,如Semaphore节点的状态期待队列中的节点有五种状态: CANCELLED:此节点对应的线程,曾经被勾销SIGNAL:此节点的下一个节点须要一个唤醒信号CONDITION:以后节点正在条件期待PROPAGATE:共享模式下会流传唤醒信号,就是说当一个线程应用共享模式拜访资源时,如果胜利拜访到资源,就会持续唤醒期待队列中的线程。自定义同步锁为了便于了解,应用AQS本人实现一个简略的同步锁,感受一下应用AQS实现同步锁是如许的轻松。 上面的代码自定了一个CustomLock类,继承了AbstractQueuedSynchronizer,并且还实现了Lock接口。CustomLock类是一个简略的可重入锁,类中只须要重写AbstractQueuedSynchronizer中的tryAcquire与tryRelease办法,而后在批改大量的调用就能够实现一个最根本的同步锁。 public class CustomLock extends AbstractQueuedSynchronizer implements Lock { @Override protected boolean tryAcquire(int arg) { int state = getState(); if(state == 0){ if( compareAndSetState(state, arg)){ setExclusiveOwnerThread(Thread.currentThread()); System.out.println("Thread: " + Thread.currentThread().getName() + "拿到了锁"); return true; } }else if(getExclusiveOwnerThread() == Thread.currentThread()){ int nextState = state + arg; setState(nextState); System.out.println("Thread: " + Thread.currentThread().getName() + "重入"); return true; } return false; } @Override protected boolean tryRelease(int arg) { int state = getState() - arg; if(getExclusiveOwnerThread() != Thread.currentThread()){ throw new IllegalMonitorStateException(); } boolean free = false; if(state == 0){ free = true; setExclusiveOwnerThread(null); System.out.println("Thread: " + Thread.currentThread().getName() + "开释了锁"); } setState(state); return free; } @Override public void lock() { acquire(1); } @Override public void unlock() { release(1); } ...}CustomLock是实现了Lock接口,所以要重写lock和unlock办法,不过办法的代码很少只须要调用AQS中的acquire和release。 ...

October 25, 2020 · 4 min · jiezi

关于并发:Go基础系列WaitGroup用法说明

失常状况下,新激活的goroutine的完结过程是不可管制的,惟一能够保障终止goroutine的行为是main goroutine的终止。也就是说,咱们并不知道哪个goroutine什么时候完结。 但很多状况下,咱们正须要晓得goroutine是否实现。这须要借助sync包的WaitGroup来实现。 WatiGroup是sync包中的一个struct类型,用来收集须要期待执行实现的goroutine。上面是它的定义: type WaitGroup struct { // Has unexported fields.} A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.A WaitGroup must not be copied after first use.func (wg *WaitGroup) Add(delta int)func (wg *WaitGroup) Done()func (wg *WaitGroup) Wait()它有3个办法: ...

October 12, 2020 · 1 min · jiezi

关于并发:Java线程的6种状态详解及创建线程的4种方式

某一天你在面试时遇到了线程的相干问题。面试官:“你晓得有哪几种创立线程的形式吗?”(此时你的心理流动:哈哈小意思这能难住我,忍住冲动伪装淡定道)你:“嗯,能够通过实现 Runnable 接口和继承 Thread 类来创立线程。”面试官:“除了这两种还有其余形式吗?”你:“emmm...还有吗?”面试官:“晓得通过实现 Callable 接口与获取 Future 对象来实现吗?”你:“emmm不晓得...不过当初晓得了嘻嘻”面试官:“那创立线程池有哪些形式呢?”你:“能够通过 ThreadPoolExecutor 构造函数或者 Executors 提供的工厂办法来创立”面试官:“那通过不同的 Executors 工厂办法创立线程池之间有什么区别呢?”你:“emmm...“面试官:“那 ThreadPoolExecutor 构造函数中的工作队列和回绝策略别离有哪些呢?”你:“emmm...“(此时你的心理流动:QAQ...不面了,把劳资简历还我!) 目录 1、线程的定义2、线程的6种状态及切换3、线程的4种创立形式3.1、实现 Runnable 接口3.2、继承 Thread 类3.3、通过 Callable、Future3.4、通过 JUC 外面的线程池4、几个常见的线程面试题注释 1、线程的定义概念:线程是过程中执行运算的最小单位,是过程中的一个实体,是被零碎独立调度和分派的根本单位,线程本人不领有系统资源,只领有一点在运行中必不可少的资源,但它可与同属一个过程的其它线程共享过程所领有的全副资源。一个线程能够创立和吊销另一个线程,同一过程中的多个线程之间能够并发执行。 2、线程6种状态及切换Java中线程的状态分为6种,定义在Thread类的State枚举中。 public class Thread implements Runnable { ... public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; } ...}NEW:初始状态,创立一个线程对象时就是该状态。RUNNABLE:运行状态,它蕴含了就绪(READY)和运行中(RUNNING)两种状态。当线程对象创立后,调用该对象的 start() 办法就会进入就绪状态(READY)。该状态的线程位于可运行线程池中,期待被线程调度选中,获取 CPU 的使用权,在取得 CPU 工夫片后会变为运行中状态(RUNNING)。BLOCKED:阻塞状态,示意线程此时阻塞于锁。WAITING:期待状态,进入该状态的线程须要期待其余线程做出一些特定动作(告诉或中断)。TIMED_WAITING:超时期待状态,该状态与 WAITING 的不同点在于它能够在指定的工夫后自行返回。TERMINATED:终止状态,示意该线程曾经执行完。 留神下图状态之间的切换。 3、线程的四种创立形式3.1、实现 Runnable 接口Runnable接口源码如下,它只有一个run()办法。 public interface Runnable { public abstract void run();}示例: ...

October 8, 2020 · 2 min · jiezi

关于并发:面试必问系列悲观锁和乐观锁的那些事儿

程序平安线程平安是程序开发中十分须要咱们留神的一环,当程序存在并发的可能时,如果咱们不做非凡的解决,很容易就呈现数据不统一的状况。 通常状况下,咱们能够用加锁的形式来保障线程平安,通过对共享资源 (也就是要读取的数据) 的加上"隔离的锁",使得多个线程执行的时候也不会相互影响,而乐观锁和乐观锁正是并发管制中较为罕用的技术手段。 乐观锁和乐观锁什么是乐观锁?什么是乐观锁?其实从字面上就能够辨别出两者的区别,艰深点说, 乐观锁乐观锁就如同一个有迫害妄想症的患者,总是假如最坏的状况,每次拿数据的时候都认为他人会批改,所以每次拿数据的时候都会上锁,直到整个数据处理过程完结,其余的线程如果要拿数据就必须等以后的锁被开释后能力操作。 应用案例 乐观锁的应用场景并不少见,数据库很多中央就用到了这种锁机制,比方行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,乐观锁的实现往往依附数据库自身的锁性能实现。Java程序中的Synchronized和ReentrantLock等实现的锁也均为乐观锁。 在数据库中,乐观锁的调用个别是在所要查问的语句前面加上 for update, select * from db_stock where goods_id = 1 for update当有一个事务调用这条 sql 语句时,会对goods_id = 1 这条记录加锁,其余的事务如果也对这条记录做 for update 的查问的话,那就必须等到该事务执行完后能力查出后果,这种加锁形式能对读和写做出排他的作用,保障了数据只能被以后事务批改。 当然,如果其余事务只是简略的查问而没有用 for update的话,那么查问还是不会受影响的,只是说更新时一样要期待以后事务完结才行。 值得注意的是,MySQL默认应用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立即将后果进行提交,就是说,如果咱们不仅要读,还要更新数据的话,须要手动管制事务的提交,比方像上面这样: set autocommit=0;//开始事务begin;//查问出商品id为1的库存表数据select * from db_stock where goods_id = 1 for update;//减库存update db_stock set stock_num = stock_num - 1 where goods_id = 1 ;//提交事务commit;尽管乐观锁能无效保证数据执行的程序性和一致性,但在高并发场景下并不实用,试想,如果一个事务用乐观锁对数据加锁之后,其余事务将不能对加锁的数据进行除了查问以外的所有操作,如果该事务执行工夫很长,那么其余事务将始终期待,这无疑会升高零碎的吞吐量。 这种状况下,咱们能够有更好的抉择,那就是乐观锁。 乐观锁乐观锁的思维和乐观锁相同,总是假如最好的状况,认为他人都是敌对的,所以每次获取数据的时候不会上锁,但更新数据那一刻会判断数据是否被更新过了,如果数据的值跟本人预期一样的话,那么就能够失常更新数据。 场景 这种思维利用到理论场景的话,能够用版本号机制和CAS算法实现。 CASCAS是一种无锁的思维,它假如线程对资源的拜访是没有抵触的,同时所有的线程执行都不须要期待,能够继续执行。如果遇到抵触的话,就应用一种叫做CAS (比拟替换) 的技术来甄别线程抵触,如果检测到抵触产生,就重试以后操作到没有抵触为止。 原理CAS的全称是Compare-and-Swap,也就是比拟并替换,它蕴含了三个参数:V,A,B,V示意要读写的内存地位,A示意旧的预期值,B示意新值 具体的机制是,当执行CAS指令的时候,只有当V的值等于预期值A时,才会把V的值改为B,如果V和A不同,有可能是其余的线程批改了,这个时候,执行CAS的线程就会一直的循环重试,直到能胜利更新为止。 ...

September 2, 2020 · 1 min · jiezi

它们为什么这么快

一直以来,计算机体现的都是它的工具价值:提高人们的效率。所以,无论是从底层硬件、中间操作系统层还是上层应用软件,速度(包括计算速度和响应速度)快始终是计算机不懈的追求。 快都是相似的,不快有各种各样的原因。 本篇就来聊一聊计算机这相似的『快』,并结合Nginx与Redis的案例来详细说明这些『快』的常见『套路』。 分时操作系统 早期计算机资源极为有限(现在也是如此且永远会如此),但是科技的成果不应该被独享,需要为大众服务。于是在20世纪70年代引入了分时的概念,让计算机可以对它的资源进行按时间片轮转(共享)的方式供多个用户使用,极大地降低了计算机成本(经济性),让个人和组织可以在不实际拥有计算机的情况下使用计算机。这种使用分时的方案为用户服务的计算机系统即为分时操作系统。 注:分时模型的引入是计算机历史上的一次重大技术革新,Unix以及类Unix操作系统都属于分时操作系统。 多进程与多线程 面对多个用户任务的请求,操作系统自然需要寻找一种优秀的模型来高效地调度它们。用户的任务映射到计算机中就是程序,而程序的执行实例即进程,所以这个优秀的模型就是进程模型,进程是分时操作系统进行资源分配和调度的基本单位,所有发生的一切均在在此基础上展开。 从用户任务到操作系统进程图 进程模型很好地解决了多任务请求的问题,且IPC(进程间通信)模型很好地解决了多任务之间的协作问题,但是计算机资源仍然是有限的(始终要记住这一点),进程有自己独立的虚拟地址空间、文件描述符以及信号处理等,单个进程运行起来仍然消耗大量的内存和处理器资源,且多进程切换的开销也很大,所以还需要再共享/复用些什么,于是线程模型便应运而生了。 线程是在进程的基础上进一步优化操作系统的调度方式,增大可以共享的粒度。一个进程内的线程除了可以拥有独立的调用栈、寄存器和本地存储,还可以共享当前进程的所有资源。多线程模型可以更大限度地利用CPU资源,在当前线程阻塞的时候可以由其他线程获取CPU执行权,从而提高系统的响应速度。如果再加上Processor Affinity(处理器亲和性/关联)特性:把任务分配到指定的CPU Core(核心)上去,这样还可以省去线程切换的开销(一个Core分配一个线程)。 进程-线程模型图 如果说一个任务对应一个进程,那么一个任务内的一个部分(子任务)则对应一个线程。如果说多进程解决的是计算问题(基于多核CPU),提高了计算性能,那么一个进程中的多线程则解决的是阻塞问题,提升了响应速度。 IO多路复用 现实场景中,多进程/线程可以一定程度上提高计算机执行效率,但是在有限的Cores上可以并发的进程/线程数会达到一定的瓶颈,即无法规模化增长:进程/线程数过少则并发性能不高;进程/线程数过多则频繁的上下文切换会带来巨大的时间开销。 例如设计一个网络应用程序,每个连接分配一个进程/线程。这种架构简单且容易实现,但是当要同时处理成千上万个连接时,服务器程序则无法规模化增长(系统资源会随着连接数增长而逐渐耗尽,Apache服务器程序就有这个问题)。同时,这也存在一个巨大的资源利用上的不对称:相当轻量级的连接(由文件描述符和少量内存表示)映射到单独的线程或进程(这是一个非常重量级的操作系统对象)。所以说,一个请求分配一个进程/线程的模式虽然容易实现,但它是对计算机资源极大的浪费。这就是著名的C10K问题(即单机支持1万个并发连接问题)。 计算机资源仍然是有限的(始终要记住这一点),如何在有限的计算机资源上让计算机执行的更快呢?优秀的程序员每天都会问计算机一遍:『还可以更快吗』。 所以,优秀的程序员们很快就会想到:是否可以让一个进程/线程处理多个连接。从而解决C10K问题的良药则是:I/O多路复用(从select到poll),对I/O异步调用而不会产生阻塞。如通过select系统调用,本来由请求线程进行的轮询操作现在改由内核负责,表面上多了一层系统调用的时间,但是由于其支持多路I/O,故提高了效率。这是高并发的真正关键所在。如果将内核的多路轮询操作改为基于事件通知的方式(即epoll),epoll会把哪个描述符发生了怎样的I/O事件通知我们,免去了轮询操作,从而进一步提高了并发性能。 上面简单论述了计算机应用程序追求更快执行的历史演进与基本原理,下面来看一下业界在『追求更快』上基于此的最佳实践。 Nginx为什么这么快 Nginx是一款业界公认的高性能Web和反向代理服务器程序,以高性能与高并发著称,官方测试结果中,其可支持五万个并发连接(在实际场景中,支持2万-4万个并发连接)。 Nginx高并发的因素大致可以归纳为以下几点: I/O多路复用:从select到poll 事件通知(异步):从poll到epoll 内存映射文件:从读文件到内存映射文件  Processor Affinity:一个Core指定分配一个进程数 进程单线程模型:避免了线程切换开销  线程池功能(1.7.1+),将可能阻塞的I/O模块扔到线程池里去Redis为什么这么快 Redis是一种应用广泛的高并发内存KV数据库,其高并发主要由以下几点保证: I/O多路复用:从select到poll 事件通知(异步):从poll到epoll 基于内存操作:读写速度非常快 单线程模式:避免了线程切换开销 多线程启用:无独有偶,类似Nginx的线程池功能,Redis4.0引入了多线程,专门用于处理一些容易阻塞的大键值对的场景。总结 通过Nginx与Redis案例可以看到,设计此类应用程序使其快的『套路』是相似的: 第一步:使用Processor Affinity特性:根据CPU核心数目确认并发的进程个数,然后将一个进程指定绑定在某一个CPU核心上。 第二步:使用进程单线程模式,每个进程-线程固定绑定在某个CPU核上执行,避免了线程切换带来的开销。且多个CPU核上的进程之间并行执行。 第三步:使用I/O多路复用,1个线程处理N个连接,这是高并发最为关键的一步。 第四步:使用事件通知,由阻塞改为非阻塞事件驱动,避免了文件描述符的轮询操作。 第五步:针对某些执行时间过长容易阻塞的场景启动线程池功能,进一步提高响应速度 第六步:针对某些具体场景的tuning,如Nginx的内存映射文件那么,除了这些,还可以更快吗?不远的将来,服务器将要处理数百万的并发连接(C10M问题)。由于CMOS 技术方法已经接近物理极限,摩尔定律即将终结,CPU则向着多核的方向发展。多核将识别并行性和决定如何利用并行性的责任转移给程序员和语言系统。于是便出现了专门用于并发场景的编程语言,如Erlang,Golang等,从一个全新的模型视角去重新审视问题,解决问题,让计算机更快。 本文首发微信公众号:yablog欢迎扫码关注

July 6, 2020 · 1 min · jiezi

并发与并行

前言 软件的运行依赖硬件基础设施。编程技术也因为硬件的不断发展而更新。通过了解计算机硬件发展,可以知道一些编程语言的设计目标发展方向。 过去半个世纪中,摩尔定律一直指导半导体与科技产业的发展。英特尔一直遵循摩尔定律,一般每隔两年左右就会发布新成果。不过现在有一些专家指出计算机已经达到摩尔定律的物理极限。 科技的不断进步,晶体管制作不断缩小,但任何物质都有物理极限。当单个物体接近物理极限时,一般会选择了多个并行运算,从而达到提升效率的目的。 摩尔定律:摩尔定律是由1965年英特尔联合创始人戈登•摩尔,他指出在微芯片上的晶体管数量大约每两年翻一番,且晶片运算性能也将随之增倍。并发与并行并发 并行经常被用来描述程序,编程。比如某系统具有并发性,某编程语言可以支持并行计算等等。它们属于系统的属性。 A system is said to be concurrent if it can support two or more actions in progress at the same time. A system is said to be parallel if it can support two or more actions executing simultaneously. The Art of Concurrency 借用 并发艺术 定义:“如果一个系统能够同时支持两个或多个进行中操作,则称该系统具有并发性。 如果系统可以支持同时执行的两个或多个操作,则称该系统为并行系统。” 例如现代操作系统,都可以同时支持多个应用同时进行中,说明它具有并发性。而要实现多个操作同时执行,需要多核CPU的硬件支持。 不同硬件环境下系统系统并发的实现有所不同。 单核单CPU,系统通过任务切换来支持执行多个任务。 多个CPU可以实现并行操作,让任务在同一物理时刻下执行。 简单来理解,可以将并行看成是实现系统并发的方式之一。 并行计算不管是多核编程,分布式编程,微服务其实都在阐述一个东西,那就是并行计算。并行计算是通过增加处理元素,从而到达缩短运行时间,提升运行效率的作用。比如通过增加CPU内核来增强计算能力。分布式里面通过增加服务器来提升吞吐量等等。 并行计算效率有对应的指导理论 阿姆达尔定律。 阿姆达尔定律: 阿姆达尔定律通常用于并行计算中以预测使用多个处理器时的理论速度。例如,如果一个程序使用单个线程需要20个小时才能完成。现在并行处理,但是该程序其中一个小时部分无法并行化,那么仅剩下的19个小时(p = 0.95)的执行时间可以并行化,无论添加多少线程用于该程序的并行执行,最短执行时间不会低于一小时。因此,理论上的并行加速最高为单线程性能的20倍。 ...

June 5, 2020 · 1 min · jiezi