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

作者:京东物流 赵帅 姚再毅 王旭东 孟伟杰 孔祥东 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

书籍翻译-JavaScript并发编程第三章-使用Promises实现同步

本文是我翻译《JavaScript Concurrency》书籍的第三章 使用Promises实现同步,该书主要以Promises、Generator、Web workers等技术来讲解JavaScript并发编程方面的实践。完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation 。由于能力有限,肯定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢。 Promises几年前就在JavaScript类库中实现了。这一切都始于Promises/A+规范。这些类库的实现都有它们自己的形式,直到最近(确切地说是ES6),Promises规范才被JavaScript语言纳入。如标题那样 - 它帮助我们实现同步原则。 在本章中,我们将首先简单介绍Promises中各种术语,以便更容易理解本章的后面部分内容。然后,通过各种方式,我们将使用Promises来解决目前的一些问题,并让并发处理更容易。准备好了吗? Promise相关术语在我们深入研究代码之前,让我们花一点时间确保我们牢牢掌握Promises有关的术语。有Promise实例,但是还有各种状态和方法。如果我们能够弄清楚Promise这些术语,那么后面的章节会更易理解。这些解释简短易懂,所以如果您已经使用过Promises,您可以快速看下这些术语,就当复习下。 Promise顾名思义,Promise是一种承诺。将Promise视为尚不存在的值的代理。Promise让我们更好的编写并发代码,因为我们知道值会在将来某个时刻存在,并且我们不必编写大量的状态检查样板代码。 状态(State)Promises总是处于以下三种状态之一: • 等待:这是Promise创建后的第一个状态。它一直处于等待状态,直到它完成或被拒绝。 • 完成:该Promise值已经处理完成,并能为它提供then()回调函数。 • 拒绝:处理Promise的值出了问题。现在没有数据。 Promise状态的一个有趣特性是它们只转换一次。它们要么从等待状态到完成,要么从等待状态到被拒绝。一旦它们进行了这种状态转换,后面就会锁定在这种状态。 执行器(Executor)执行器函数负责以某种方式解析值并将处于等待状态。创建Promise后立即调用此函数。它需要两个参数:resolver函数和rejector函数。 解析器(Resolver)解析器是一个作为参数传递给执行器函数的函数。实际上,这非常方便,因为我们可以将解析器函数传递给另一个函数,依此类推。调用解析器函数的位置并不重要,但是当它被调用时,Promise会进入一个完成状态。状态的这种改变将触发then()回调 - 这些我们将在后面看到。 拒绝器(Rejector)拒绝器与解析器相似。它是传递给执行器函数的第二个参数,可以从任何地方调用。当它被调用时,Promise从等待状态改变到拒绝状态。这种状态的改变将调用错误回调函数,如果有的话,会传递给then()或catch()。 Thenable如果对象具有接受完成回调和拒绝回调作为参数的then()方法,则该对象就是Thenable。换句话说,Promise是Thenable。但是在某些情况下,我们可能希望实现特定的解析语义。 完成和拒绝Promises如果上一节刚刚介绍的几个术语听起来让你困惑,那别担心。从本节开始,我们将看到所有这些Promises术语的应用实践。在这里,我们将展示一些简单的Promise解决和拒绝的示例。 完成Promises解析器是一个函数,顾名思义,它完成了我们的Promise。这不是完成Promise的唯一方法 - 我们将在后面探索更高级的方式。但到目前为止,这种方法是最常见的。它作为第一个参数传递给执行器函数。这意味着执行器可以通过简单地调用解析器直接完成Promise。但这并不怎么实用,不是吗? 更常见的情况是Promise执行器函数设置即将发生的异步操作 - 例如拨打网络电话。然后,在这些异步操作的回调函数中,我们可以完成这个Promise。在我们的代码中传递一个解析函数,刚开始可能感觉有点违反直觉,但是一旦我们开始使用它们就会发现很有意义。 解析器函数是一个相对Promise来说比较难懂的函数。它只能完成一次Promise。我们可以调用解析器很多次,但只在第一次调用会改变Promise的状态。下面是一个图描述了Promise的可能状态;它还显示了状态之间是如何变化的: 现在,我们来看一些Promise代码。在这里,我们将完成一个promise,它会调用then()完成回调函数: //我们的Promise使用的执行器函数。//第一个参数是解析器函数,在1秒后调用完成Promise。function executor(resolve) { setTimeout(resolve, 1000);}//我们Promise的完成回调函数。//这个简单地在我们的执行程序函数运行后,停止那个定时器。function fulfilled() { console.timeEnd('fulfillment');}//创建promise,并立即运行,//然后启动一个定时器来查看调用完成函数需要多长时间。var promise = new Promise(executor);promise.then(fulfilled);console.time('fulfillment');我们可以看到,解析器函数被调用时fulfilled()函数会被调用。执行器实际上并不调用解析器。相反,它将解析器函数传递给另一个异步函数 - setTimeout()。执行器并不是我们试图去弄清楚的异步代码。可以将执行器视为一种协调程序,它编排异步操作并确定何时执行Promise。 前面的示例未解析任何值。当某个操作的调用者需要确认它成功或失败时,这是一个有效的用例。相反,让我们这次尝试解析一个值,如下所示: //我们的Promise使用的执行函数。//创建Promise后,设置延时一秒钟调用"resolve()",//并解析返回一个字符串值 - "done!"。function executor(resolve) { setTimeout(() => { resolve('done!'); }, 1000);}//我们Promise的完成回调接受一个值参数。//这个值将传递到解析器。function fulfilled(value) { console.log('resolved', value);}//创建我们的Promise,提供执行程序和完成回调函数。var promise = new Promise(executor);promise.then(fulfilled);我们可以看到这段代码与前面的例子非常相似。区别在于我们的解析器函数实际上是在传递给setTimeout()的回调函数的闭包内调用的。这是因为我们正在解析一个字符串值。还有一个将被解析的参数值传递给我们的fulfilled()函数。 ...

October 17, 2019 · 5 min · jiezi

java-并发编程之共享变量

可见性如果一个线程对共享变量值的修改, 能够及时的被其他线程看到, 叫做共享变量的可见性. Java 虚拟机规范试图定义一种 Java 内存模型 (JMM), 来屏蔽掉各种硬件和操作系统的内存访问差异, 让 Java 程序在各种平台上都能达到一致的内存访问效果. 简单来说, 由于 CPU 执行指令的速度是很快的, 但是内存访问的速度就慢了很多, 相差的不是一个数量级, 所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存. 在 Java 内存模型里, 对上述的优化又进行了一波抽象. JMM 规定所有变量都是存在主存中的, 类似于上面提到的普通内存, 每个线程又包含自己的工作内存, 方便理解就可以看成 CPU 上的寄存器或者高速缓存. 所以线程的操作都是以工作内存为主, 它们只能访问自己的工作内存, 且工作前后都要把值在同步回主内存. 简单点就是, 多线程中读取或修改共享变量时, 首先会读取这个变量到自己的工作内存中成为一个副本, 对这个副本进行改动后, 再更新回主内存中. 使用工作内存和主存, 虽然加快的速度, 但是也带来了一些问题. 比如看下面一个例子: i = i + 1;假设 i 初值为 0, 当只有一个线程执行它时, 结果肯定得到 1, 当两个线程执行时, 会得到结果 2 吗? 这倒不一定了. 可能存在这种情况: 线程1: load i from 主存 // i = 0 i + 1 // i = 1线程2: load i from主存 // 因为线程1还没将i的值写回主内存,所以i还是0 i + 1 //i = 1线程1: save i to 主存线程2: save i to 主存如果两个线程按照上面的执行流程, 那么 i 最后的值居然是 1 了. 如果最后的写回生效的慢, 你再读取 i 的值, 都可能是 0, 这就是缓存不一致问题. ...

September 20, 2019 · 2 min · jiezi

Java并发ReentrantLock

1.简介可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似,但是功能上比 synchronized 更强大,除可重入之外,ReentrantLock还具有4个特性:等待可中断、可实现公平锁、可设置超时、以及锁可以绑定多个条件。在synchronized不能满足的场景下,如公平锁、允许中断、需要设置超时、需要多个条件变量的情况下,需要考虑使用ReentrantLock。 2.用法ReenTrantLock继承了Lock接口,Lock接口声明有如下方法: 2.1 可重入锁可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。 void m1() { lock.lock();try { // 调用 m2,因为可重入,所以并不会被阻塞 m2();} finally { lock.unlock()}} void m2() { lock.lock();try { // do something} finally { lock.unlock()}} 注:ReentrantLock的方法需要置于try-finally块中,需要在finally中释放锁,防止因方法异常锁无法释放。 2.2 可中断锁在等待获取锁过程中可中断。注意是在等待锁过程中才可以中断,如果已经获取了锁,中断就无效。调用锁的lockInterruptibly方法即可实现可中断锁,当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。示例如下: public class ReentrantLockTest { private static int account = 0;private static ReentrantLock lock = new ReentrantLock();public static void main (String [] args) { Thread t1 = new Thread(()->{ try { lock.lockInterruptibly(); System.out.println("线程t1输出:"+account++); } catch (InterruptedException e) { System.out.println("线程t1被中断了"); }finally { lock.unlock(); } },"t1"); Thread t2 = new Thread(()->{ try { lock.lockInterruptibly(); System.out.println("线程t2输出:"+account++); // 调用interrupt方法中断线程t1 t1.interrupt(); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("线程t2被中断了"); }finally { lock.unlock(); } },"t2"); t2.start(); t1.start(); }} ...

September 19, 2019 · 3 min · jiezi

手撕ThreadPoolExecutor线程池源码

这篇文章对ThreadPoolExecutor创建的线程池如何操作线程的生命周期通过源码的方式进行详细解析。通过对execute方法、addWorker方法、Worker类、runWorker方法、getTask方法、processWorkerExit从源码角度详细阐述,文末有彩蛋。exexcte方法public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); /** * workerCountOf方法取出低29位的值,表示当前活动的线程数; * 如果当前活动的线程数小于corePoolSize,则新建一个线程放入线程池中,并把该任务放到线程中 */ if (workerCountOf(c) < corePoolSize) { /** * addWorker中的第二个参数表示限制添加线程的数量 是根据据corePoolSize 来判断还是maximumPoolSize来判断; * 如果是ture,根据corePoolSize判断 * 如果是false,根据maximumPoolSize判断 */ if (addWorker(command, true)) return; /** * 如果添加失败,则重新获取ctl值 */ c = ctl.get(); } /** * 如果线程池是Running状态,并且任务添加到队列中 */ if (isRunning(c) && workQueue.offer(command)) { //double-check,重新获取ctl的值 int recheck = ctl.get(); /** * 再次判断线程池的状态,如果不是运行状态,由于之前已经把command添加到阻塞队列中,这时候需要从队列中移除command; * 通过handler使用拒绝策略对该任务进行处理,整个方法返回 */ if (!isRunning(recheck) && remove(command)) reject(command); /** * 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法; * 第一个参数为null,表示在线程池中创建一个线程,但不去启动 * 第二个参数为false,将线程池的线程数量的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断 */ else if (workerCountOf(recheck) == 0) addWorker(null, false); /** * 执行到这里,有两种情况: * 1、线程池的状态不是RUNNING; * 2、线程池状态是RUNNING,但是workerCount >= corePoolSize, workerQueue已满 * 这个时候,再次调用addWorker方法,第二个参数传false,将线程池的有限线程数量的上限设置为maximumPoolSize; * 如果失败则执行拒绝策略; */ } else if (!addWorker(command, false)) reject(command);}简单来说,在执行execute()方法时如果状态一直是RUNNING时,的执行过程如下: ...

August 18, 2019 · 4 min · jiezi

Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。线程实现方式Thread、Runnable、Callable //实现Runnable接口的类将被Thread执行,表示一个基本任务public interface Runnable { //run方法就是它所有内容,就是实际执行的任务 public abstract void run();}//Callable同样是任务,与Runnable接口的区别在于它接口泛型,同时它执行任务候带有返回值;//Callable的使用通过外层封装成Future来使用public interface Callable<V> { //相对于run方法,call方法带有返回值 V call() throws Exception;}注意:启动Thread线程只能用start(JNI方法)来启动,start方法通知虚拟机,虚拟机通过调用器映射到底层操作系统,通过操作系统来创建线程来执行当前任务的run方法 Executor框架Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。从图中可以看出Exectuor下有一个重要的子接口ExecutorService,其中定义了线程池的具体行为: execute(Runnable runnable):执行Runnable类型的任务submit(task):用来提交Callable或者Runnable任务,并返回代表此任务的Future对象shutdown():在完成已经提交的任务后封闭办事,不在接管新的任务shutdownNow():停止所有正在履行的任务并封闭办事isTerminated():是一个钩子函数,测试是否所有任务都履行完毕了isShutdown():是一个钩子函数,测试是否该ExecutorService是否被关闭 ExecutorService中的重点属性:private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));private static final int COUNT_BITS = Integer.SIZE - 3;private static final int CAPACITY = (1 << COUNT_BITS) - 1;ctl:对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,它包含两部分信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount)。 这里可以看到,使用Integer类型来保存,高3位保存runState,低29位保存workerCount。COUNT_BITS 就是29,CAPACITY 就是1左移29位减1(29个1),这个常量表示workerCount的上限值,大约是5亿。 ctl相关方法: //获取运行状态private static int runStateOf(int c) { return c & ~CAPACITY; }//获取活动线程数private static int workerCountOf(int c) { return c & CAPACITY; }//获取运行状态和活动线程数的值private static int ctlOf(int rs, int wc) { return rs | wc; }线程池的状态: ...

August 18, 2019 · 2 min · jiezi

Erlang-源码阅读-scheduler

移步 https://ruby-china.org/topics...

August 8, 2019 · 1 min · jiezi

ForkJoin-框架详解基于-JDK-8

概述Fork 就是把一个大任务切分为若干个子任务并行地执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。Fork/Join 框架使用的是工作窃取算法。 工作窃取算法工作窃取算法是指某个线程从其他队列里窃取任务来执行。对于一个比较大的任务,可以把它分割为若干个互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务需要处理,于是它就去其他线程的队列里窃取一个任务来执行。由于此时它们访问同一个队列,为了减小竞争,通常会使用双端队列。被窃取任务的线程永远从双端队列的头部获取任务,窃取任务的线程永远从双端队列的尾部获取任务。 工作窃取算法的优缺点优点:充分利用线程进行并行计算,减少了线程间的竞争。缺点:双端队列只存在一个任务时会导致竞争,会消耗更多的系统资源,因为需要创建多个线程和多个双端队列。 Fork/Join 框架的异常处理ForkJoinTask 在执行的时候可能抛出异常,但没有办法在主线程中直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法检查任务是否已经抛出异常或已经被取消。getException() 方法返回 Throwable 对象,如果任务被取消了则返回 CancellationException,如果任务没有完成或者没有抛出异常则返回 null。 Fork/Join 框架的实现原理fork() 方法的实现原理当调用 ForkJoinTask 的 fork() 方法时,程序会调用 ForkJoinPool.WorkQueue 的 push() 方法异步地执行这个任务,然后立即返回结果。代码如下: public final ForkJoinTask<V> fork() { Thread t; if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ((ForkJoinWorkerThread)t).workQueue.push(this); else ForkJoinPool.common.externalPush(this); return this;}push() 方法把当前任务存放在一个 ForkJoinTask 数组队列里,然后再调用 ForkJoinPool 的 signalWork() 方法唤醒或创建一个工作线程来执行任务。代码如下: final void push(ForkJoinTask<?> task) { ForkJoinTask<?>[] a; ForkJoinPool p; int b = base, s = top, n; if ((a = array) != null) { // ignore if queue removed int m = a.length - 1; // fenced write for task visibility U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task); U.putOrderedInt(this, QTOP, s + 1); if ((n = s - b) <= 1) { if ((p = pool) != null) p.signalWork(p.workQueues, this); } else if (n >= m) growArray(); }}join() 方法的实现原理当调用 ForkJoinTask 的 join() 方法时,程序会调用 doJoin() 方法,通过 doJoin() 方法来判断返回什么结果 ...

July 6, 2019 · 2 min · jiezi

小马哥Java面试题课程总结

前段时间在慕课网直播上听小马哥面试劝退("面试虐我千百遍,Java 并发真讨厌"),发现讲得东西比自己拿到offer还要高兴,于是自己在线下做了一点小笔记,文章还没更新完,供各位参考。 课程地址:https://www.bilibili.com/vide... 源码文档地址:https://github.com/mercyblitz... 本文来自于我的慕课网手记:小马哥Java面试题课程总结,转载请保留链接 ;)Java 多线程1、线程创建基本版有哪些方法创建线程? 仅仅只有new thread这种方法创建线程 public class ThreadCreationQuestion { public static void main(String[] args) { // main 线程 -> 子线程 Thread thread = new Thread(() -> { }, "子线程-1"); } /** * 不鼓励自定义(扩展) Thread */ private static class MyThread extends Thread { /** * 多态的方式,覆盖父类实现 */ @Override public void run(){ super.run(); } }}与运行线程方法区分:java.lang.Runnable() 或 java.lang.Thread类 进阶版如何通过Java 创建进程? public class ProcessCreationQuestion { public static void main(String[] args) throws IOException { // 获取 Java Runtime Runtime runtime = Runtime.getRuntime(); Process process = runtime.exec("cmd /k start http://www.baidu.com"); process.exitValue(); }}劝退版如何销毁一个线程? ...

June 24, 2019 · 12 min · jiezi

lua-web快速开发指南7-高效的接口调用-httpc库

httpc库基于cf框架都内部实现的socket编写的http client库. httpc库内置SSL支持, 在不使用代理的情况下就可以请求第三方接口. httpc支持header、args、body、timeout请求设置, 完美支持各种httpc调用方式. API介绍httpc库使用前需要手动导入httpc库: local httpc = require "httpc". httpc.get(domain, HEADER, ARGS, TIMEOUT)调用get方法将会对domain发起一次HTTP GET请求. domain是一个符合URL定义规范的字符串; HEADER是一个key-value数组, 一般用于添加自定义头部; ARGS为请求参数的key-value数组, 对于GET方法将会自动格式化为:args[n][1]=args[n][2]&args[n+1][1]=args[n+1][2]; TIMEOUT为httpc请求的最大超时时间; httpc.post(domain, HEADER, BODY, TIMEOUT)调用post方法将会对domain发起一次HTTP POST请求, 此方法的content-type会被设置为:application/x-www-form-urlencoded. domain是一个符合URL定义规范的字符串; HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; BODY是一个key-value数组, 对于POST方法将会自动格式化为:body[n][1]=body[n][2]&body[n+1][1]=body[n+1][2]; TIMEOUT为httpc请求的最大超时时间; httpc.json(domain, HEADER, JSON, TIMEOUT)json方法将会对domain发起一次http POST请求. 此方法的content-type会被设置为:application/json. HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; JSON必须是一个字符串类型; TIMEOUT为httpc请求的最大超时时间; httpc.file(domain, HEADER, FILES, TIMEOUT)file方法将会对domain发起一次http POST请求. HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; FILES是一个key-value数组, 每个item包含: name(名称), filename(文件名), file(文件内容), type(文件类型)等属性. 文件类型可选. TIMEOUT为httpc请求的最大超时时间; httpc 返回值所有httpc请求接口均会有2个返回值: code, response. code为http协议状态码, response为回应body(字符串类型). ...

June 16, 2019 · 4 min · jiezi

Java并发12-原子类无锁工具类的典范

前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都是采用的互斥锁方案。 public class Test { long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } }}其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。 在下面的代码中,我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使 add10K() 方法变成线程安全的,原子类的使用还是挺简单的。 public class Test { AtomicLong count = new AtomicLong(0); void add10K() { int idx = 0; while(idx++ < 10000) { count.getAndIncrement(); } }}无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。那它是如何做到的呢? ...

June 16, 2019 · 3 min · jiezi

全栈之路JAVA基础课程四哲学家就餐问题20190614v12

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:“天眼”之父南仁东,心无旁骛,为崇山峻岭间的中国“天眼”燃尽生命,看似一口“大锅”,天眼是世界上最大、最灵敏的单口径射电望远镜,可以接受百亿光年外的电磁信号。南仁东总工程师执着追求科学梦想的精神,将激励一代又一代科技工作者继续奋斗,勇攀世界科技高峰。作为IT界的从业者,我们需要紧跟时代的步伐,踏过平庸,一生为科技筑梦。生产者消费者问题1. 背景 有五个哲学家,他们的生活方式是交替地进行思考和进餐,哲学家们共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子,平时哲学家进行思考,饥饿时便试图取其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐,进餐完毕,放下筷子又继续思考。 2. 代码实现 //定义哲学家类,每个哲学家相当于一个线程class Philosopher extends Thread{ private String name; private Fork fork; public Philosopher(String name,Fork fork){ super(name); this.name=name; this.fork=fork; } public void run(){//每个哲学家的动作,思考-》拿起跨骑 while (true){ thinking(); fork.takeFork(); eating(); fork.putFork(); } } //模拟思考 public void thinking(){ System.out.println("我在思考:"+name); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //模拟吃放 public void eating(){ System.out.println("我在吃:"+name); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}class Fork{ //5双筷子,初始未未使用 private boolean[] used={false,false,false,false,false}; //拿起筷子 public synchronized void takeFork(){ String name=Thread.currentThread().getName(); int i=Integer.parseInt(name); while (used[i]||used[(i+1)%5]){//左右手有一只被使用则等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } used[i]=true; used[(i+1)%5]=true; } //释放筷子 public synchronized void putFork(){ String name=Thread.currentThread().getName(); int i=Integer.parseInt(name); used[i]=false; used[(i+1)%5]=false; notifyAll(); }}public class PhilosopherMain { public static void main(String[] args) { Fork fork=new Fork(); new Philosopher("0",fork).start(); new Philosopher("1",fork).start(); new Philosopher("2",fork).start(); new Philosopher("3",fork).start(); new Philosopher("4",fork).start(); }}输出结果 ...

June 14, 2019 · 1 min · jiezi

全栈之路JAVA基础课程四20190614v11

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:“天眼”之父南仁东,心无旁骛,为崇山峻岭间的中国“天眼”燃尽生命,看似一口“大锅”,天眼是世界上最大、最灵敏的单口径射电望远镜,可以接受百亿光年外的电磁信号。南仁东总工程师执着追求科学梦想的精神,将激励一代又一代科技工作者继续奋斗,勇攀世界科技高峰。作为IT界的从业者,我们需要紧跟时代的步伐,踏过平庸,一生为科技筑梦。生产者消费者问题1. 背景 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。 2. 条件 生产者仅仅在仓储未满时候生产, 仓满则停止生产.生产者在生产出可消费产品时候, 应该通知等待的消费者去消费.消费者仅仅在仓储有产品时候才能消费, 仓空则等待.消费者发现仓储没产品可消费时候会通知生产者生产.3.实现方式 wait() / notify()方法await() / signal()方法BlockingQueue阻塞队列方法Semaphore方法PipedInputStream / PipedOutputStream下面主要针对前面三种方式做代码实现 (1) wait() / notify()方法 public class ProducerMain { private static Integer count=0; private final Integer full=10; private static String LOCK="LOCK"; class Producer implements Runnable{ @Override public void run() {// for(int i=0;i<10;i++){// try {// Thread.sleep(3000);// } catch (InterruptedException e) {// e.printStackTrace();// }// } synchronized (LOCK){ while(count==full){ try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } count++; System.out.println(Thread.currentThread().getName()+"生产者生产,目前共生产了:"+count); LOCK.notifyAll(); } } } class Consumer implements Runnable{ @Override public void run() { synchronized (LOCK){ while (count==0){ try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } count--; System.out.println(Thread.currentThread().getName()+"生产者消费,目前共剩余:"+count); LOCK.notifyAll(); } } } public static void main(String[] args) { ProducerMain producer = new ProducerMain(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); new Thread(producer.new Producer()).start(); new Thread(producer.new Consumer()).start(); }}输出结果 ...

June 14, 2019 · 3 min · jiezi

线程池没你想的那么简单续

前言前段时间写过一篇《线程池没你想的那么简单》,和大家一起撸了一个基本的线程池,具备: 线程池基本调度功能。线程池自动扩容缩容。队列缓存线程。关闭线程池。这些功能,最后也留下了三个待实现的 features 。 执行带有返回值的线程。异常处理怎么办?所有任务执行完怎么通知我?这次就实现这三个特性来看看 j.u.c 中的线程池是如何实现这些需求的。 再看本文之前,强烈建议先查看上文《线程池没你想的那么简单》任务完成后的通知大家在用线程池的时候或多或少都会有这样的需求: 线程池中的任务执行完毕后再通知主线程做其他事情,比如一批任务都执行完毕后再执行下一波任务等等。 以我们之前的代码为例: 总共往线程池中提交了 13 个任务,直到他们都执行完毕后再打印 “任务执行完毕” 这个日志。执行结果如下: 为了简单的达到这个效果,我们可以在初始化线程池的时候传入一个接口的实现,这个接口就是用于任务完成之后的回调。 public interface Notify { /** * 回调 */ void notifyListen() ;}以上就是线程池的构造函数以及接口的定义。 所以想要实现这个功能的关键是在何时回调这个接口? 仔细想想其实也简单:只要我们记录提交到线程池中的任务及完成的数量,他们两者的差为 0 时就认为线程池中的任务已执行完毕;这时便可回调这个接口。 所以在往线程池中写入任务时我们需要记录任务数量: 为了并发安全的考虑,这里的计数器采用了原子的 AtomicInteger 。 而在任务执行完毕后就将计数器 -1 ,一旦为 0 时则任务任务全部执行完毕;这时便可回调我们自定义的接口完成通知。 JDK 的实现这样的需求在 jdk 中的 ThreadPoolExecutor 中也有相关的 API ,只是用法不太一样,但本质原理都大同小异。 我们使用 ThreadPoolExecutor 的常规关闭流程如下: executorService.shutdown(); while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) { logger.info("thread running"); }线程提交完毕后执行 shutdown() 关闭线程池,接着循环调用 awaitTermination() 方法,一旦任务全部执行完毕后则会返回 true 从而退出循环。 ...

June 6, 2019 · 2 min · jiezi

工作记录-登录短信验证码防刷解决思路

一、写在前面在互联网的发展史上,安全总是一个绕不开话题, 你有安全盾、我有破盾矛。所谓道高一尺、魔高一丈,不过互联网安全也正是在这种攻防中慢慢的发展起来的。 不过今天写的没有上面说的那么高大,只是一个小小的防刷解决思路。 这是工作中经常遇到的、在此仅做一个记录,以便回顾。 如有不严谨或者不完善的地方,欢迎指正 ~谢谢~ 二、场景引入、问题凸显场景: 在我们给 xx 做的区块链共享出行平台中区块链浏览器系统的登录是以用户的手机号+验证码来登录的。 因是内部用户使用、在登录这块也没做特殊的安全处理,导致在测试时被我们的测试小哥给刷爆了(在这给测试小哥点个赞)。 到这我们必然的收到一个bug了,业务期望。 1: 同一个ip限制一分钟最多获取5次 2: 超过5次则锁定1小时,锁定期间获取短信需加图片验证码 收到这个需求、利用Redis做了简单的限流防刷功能。 三、解决方案首先分析需求, 1: 对同一IP做限制 2: 对单位时间内次数做限制 利用Redis来实现思路 1: 一个获取短信验证码请求过来我们首先的判断此IP是否已经被锁定(单位时间内超过了限定的访问次数) 2: 如果未被锁定则判断此IP是否是首次访问,如果是则给此IP加个生命周期以及记录访问次数。 3: 如果不是首次访问,则判断单位时间内是否符合限制要求。 完成这三不我们需要在 redis 中定义三个KEY msg_lock_key_{ip} 记录次IP已被锁定 msg_time_key_{ip} 记录单位时间内IP msg_counter_key_{ip} 记录单位时间内IP访问次数 到这就直接上一段代码吧, public boolean checkMsgFrequency(String remotIp) { String romteIpString = remotIp.replace(".", ""); // 1: 判断此ip是否已经被限制 String msgLockKey = Constants.API_MSG_LOCK_KEY + romteIpString; if (jedis.exists(msgLockKey)) { // 此ip已经被锁住 logger.info("此Ip[{}]以超过规定访问频率key:{}", remotIp, msgLockKey); return false; } // 2: 判断此ip是否在规定的时间内访问过 String msgTimeKey = Constants.API_MSG_TIME_KEY + romteIpString; String msgCounterKey = Constants.API_MSG_COUNTER_KEY + romteIpString; // key 不存在 if (!jedis.exists(msgTimeKey)) { // 加入缓存 jedis.setEx(msgTimeKey, "0", imageCode.getLimitTime()); jedis.setEx(msgCounterKey, "1", imageCode.getLimitTime()); } if (jedis.exists(msgTimeKey) && (jedis.incrBy(msgCounterKey, 1) > imageCode.getLimitCounter())) { logger.info("此Ip[{}]以超过规定访问频率、进行枷锁key:{}", remotIp, msgTimeKey); jedis.setEx(msgLockKey, "0", imageCode.getLimitlockTime()); return false; } return true; }这是一个简单的代码实现, 逻辑就是这个逻辑,实现方式很多种。 ...

May 30, 2019 · 1 min · jiezi

Java并发11-并发容器的使用

Java 并发包有很大一部分内容都是关于并发容器的,因此学习和搞懂这部分的内容很有必要。 Java 1.5 之前提供的同步容器虽然也能保证线程安全,但是性能很差,而 Java 1.5 版本之后提供的并发容器在性能方面则做了很多优化,并且容器的类型也更加丰富了。下面我们就对比二者来学习这部分的内容。 同步容器及其注意事项Java 中的容器主要可以分为四个大类,分别是 List、Map、Set 和 Queue,但并不是所有的 Java 容器都是线程安全的。例如,我们常用的 ArrayList、HashMap 就不是线程安全的。在介绍线程安全的容器之前,我们先思考这样一个问题:如何将非线程安全的容器变成线程安全的容器? 之前我们讨论果,只要把非线程安全的容器封装在对象内部,然后控制好访问路径就可以了。 下面我们就以 ArrayList 为例,看看如何将它变成线程安全的。在下面的代码中,SafeArrayList 内部持有一个 ArrayList 的实例 c,所有访问 c 的方法我们都增加了 synchronized 关键字,需要注意的是我们还增加了一个 addIfNotExist() 方法,这个方法也是用 synchronized 来保证原子性的。 SafeArrayList<T>{ // 封装 ArrayList List<T> c = new ArrayList<>(); // 控制访问路径 synchronized T get(int idx){ return c.get(idx); } synchronized void add(int idx, T t) { c.add(idx, t); } synchronized boolean addIfNotExist(T t){ if(!c.contains(t)) { c.add(t); return true; } return false; }}看到这里,你可能会举一反三,然后想到:所有非线程安全的类是不是都可以用这种包装的方式来实现线程安全呢?其实在Java SDK就提供了这类功能,在 Collections 这个类中还提供了一套完备的包装类,比如下面的示例代码中,分别把 ArrayList、HashSet 和 HashMap 包装成了线程安全的 List、Set 和 Map。 ...

May 26, 2019 · 2 min · jiezi

AQS同步组件ReentrantLock与锁

ReentrantLock与锁Synchronized和ReentrantLock异同可重入性:两者都具有可重入性锁的实现:Synchronized是依赖jvm实现的,ReentrantLock是jdk实现的。(我们可以理解为一个是操作系统层面的实现另一个是用户自己自己实现的)Synchronized的实现是jvm层面的很难看到其中的实现。而ReentrantLock是通过jvm实现的我们可以通过阅读jvm源码来查看实现。性能区别:在Synchronized优化之前Synchronized的性能相比ReentrantLock差很多,在Synchronized引入了偏向锁,轻量级锁也就是自旋锁之后了,两者的性能相差不大了。在两者都可用的情况下官方更推荐使用Synchronized,因为其写法更简单,Synchronized的优化就是借鉴了ReentrantLock中的cas技术。功能区别:便利性,很明显synchronized的使用更加便利,ReentrantLock在细粒度和灵活性中会优于Synchronized。ReentrantLock独有功能ReentrantLock可指定是公平锁还是非公平锁,Synchronized只能是非公平锁。(公平锁就是先等待的线程先获得锁)ReentrantLock提供一个Condition类,可以分组唤醒需要唤醒的形成。synchronized是要嘛随机唤醒一个线程要嘛唤醒所有的线程。ReentrantLock提供了一种能够中断等待锁的线程的机制lock.locInterruptibly(),ReentrantLock实现是一种自旋锁通过循环调用,通过cas机制来实现加锁。性能较好是因为避免了线程进入内核的阻塞状态@Slf4jpublic class LockExample2 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static int count = 0; private final static Lock lock = new ReentrantLock(); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private static void add() { lock.lock(); try { count++; } finally { lock.unlock(); } }}我们首先使用 private final static Lock lock = new ReentrantLock()声明一个所得实例,然后使用 ...

May 15, 2019 · 3 min · jiezi

AQS同步组件CyclicBarrier

CyclicBarrierCyclicBarrier也是一个同步辅助类,它允许一组线程相互等待直到到达某个工作屏障点,通过他可以完成多线程之间的相互等待。每个线程都就绪之后才能执行后面的操作。和CountLatch有相似的地方都是通过计数器来实现的。当某个线程执行了await()方法后就进入等待状态,计数器进行加1操作,当增加后的值达到我们设定的值后,线程被唤醒,继续执行后续操作。CyclicBarrier是可重用的计数器,CyclicBarrier的使用场景和CountDownLatch的使用场景很相似,可以用于多线程计算数据最后总计结果。CyclicBarrier和CountDownLatch的使用区别: CountDownLatch的计数器只能使用一次,CyclicBarrier可以用reset方法重置。CountDownLatch是一个线程等待其他线程完成某个操作后才能继续执行。也就是一个或个多线程等待其他的关系,而CyclicBarrier是实现了多个线程之间的相互等待,所有线程都满足了条件之后才能继续使用。演示代码 @Slf4jpublic class CyclicBarrierExample1 { private static CyclicBarrier barrier = new CyclicBarrier(5); public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int threadNum = i; Thread.sleep(1000); executor.execute(() -> { try { race(threadNum); } catch (Exception e) { log.error("exception", e); } }); } executor.shutdown(); } private static void race(int threadNum) throws Exception { Thread.sleep(1000); log.info("{} is ready", threadNum); barrier.await(); log.info("{} continue", threadNum); }}输出结果如下: ...

May 14, 2019 · 2 min · jiezi

AQS同步组件Semaphore

Semaphore什么是Semaphore?是用于控制某个资源同一时间被线程访问的个数,提供acquire()和release()方法,acquire获取一个许可,如果没有获取的到就等待,release是在操作完成后释放一个许可,Semaphore维护了当前访问的个数,通过同步机制来控制同时访问的个数,在数据结构里链表中的节点是可以无限个的,而Semaphore里维护的是一个有大小的限链表。Semaphore的使用场景Semaphore用于仅能提供有限访问的资源,比如数据库中的链接数只有20但是我们上层应用数可能远大于20,如果同时都对数据库链接进行获取,那很定会因为链接获取不到而报错,所以我们就要对数据库链接的访问进行控制。演示代码@Slf4jpublic class SemaphoreExample1 { private final static int threadCount = 20; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(3); for (int i = 0; i < threadCount; i++) { final int threadNum = i; exec.execute(() -> { try { semaphore.acquire(); // 获取一个许可 test(threadNum); semaphore.release(); // 释放一个许可 } catch (Exception e) { log.error("exception", e); } }); } exec.shutdown(); } private static void test(int threadNum) throws Exception { log.info("{}", threadNum); Thread.sleep(1000); }}我们在执行 test(threadNum)方式前后包裹上acquire和release,这样其实我们就相当于一个单线程在执行。当执行acquire后就只能等待执行release后再执行新的线程,然后我们在acquire()和release()都是没有传参也就是1,每次只允许一个线程执行,如果我们改成 ...

May 13, 2019 · 2 min · jiezi

AQS同步组件CountDownLatch

CountDownLatchCountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。 调用CountDownLatch类的await()方法会一直阻塞,直到其他线程调用CountDown()方法使计数器的值减1,当计数器的值等于0则当因调用await()方法处于阻塞状态的线程会被唤醒继续执行。计数器是不能被重置的。这个类使用线程在达到某个条件后继续执行的情况。比如并行计算,计算量特别大,我可以将计算量拆分成多个线程进行计算,最后将结果汇总。 一个CountdownLatch 例子 @Slf4jpublic class CountDownLatchExample1 { private final static int threadCount = 200; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadNum = i; exec.execute(() -> { try { test(threadNum); } catch (Exception e) { log.error("exception", e); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); log.info("finish"); exec.shutdown(); } private static void test(int threadNum) throws Exception { Thread.sleep(100); log.info("{}", threadNum); Thread.sleep(100); }}我们在线程之后都调用了countDown方法,在执行log之前调用了await方法,从而来保证打印日志时一定是在所有线程执行完。假设我们不使用CountDownLatch时结果会怎么样? ...

May 12, 2019 · 1 min · jiezi

Java并发7java的线程小节

在 Java 领域,实现并发程序的主要手段就是多线程。线程是操作系统里的一个概念,虽然各种不同的开发语言如 Java、C# 等都对其进行了封装,但原理和思路都是相同都。Java 语言里的线程本质上就是操作系统的线程,它们是一一对应的。 在操作系统层面,线程也有“生老病死”,专业的说法叫有生命周期。对于有生命周期的事物,要学好它,只要能搞懂生命周期中各个节点的状态转换机制就可以了。 虽然不同的开发语言对于操作系统线程进行了不同的封装,但是对于线程的生命周期这部分,基本上是雷同的。所以,我们可以先来了解一下通用的线程生命周期模型,然后再详细的学习一下 Java 中线程的生命周期。 通用的线程生命周期通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是: 初始状态、可运行状态、运行状态、休眠状态 和 终止状态通用线程状态转换图——五态模型 初始状态:指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。可运行状态:指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。运行状态:当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态休眠状态:运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到 休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。终止状态:线程执行完或者出现异常就会进入 终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。这五种状态在不同编程语言里会有简化合并或者被细化。Java 中线程的生命周期Java 语言中线程共有六种状态,分别是: NEW(初始化状态)RUNNABLE(可运行 / 运行状态)BLOCKED(阻塞状态)WAITING(无时限等待)TIMED_WAITING(有时限等待)TERMINATED(终止状态)在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说 只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。所以 Java 线程的生命周期可以简化为下图: Java 中的线程状态转换图 其中,BLOCKED、WAITING、TIMED_WAITING 可以理解为线程导致休眠状态的三种原因。那具体是哪些情形会导致线程从 RUNNABLE 状态转换到这三种状态呢?而这三种状态又是何时转换回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 状态是如何转换的? 1. RUNNABLE 与 BLOCKED 的状态转换只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。而当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。 ...

May 11, 2019 · 3 min · jiezi

程序员笔记如何编写高性能的Java代码

一、并发无法创建新的本机线程...... 问题1:Java的中创建一个线程消耗多少内存? 每个线程有独自的栈内存,共享堆内存 问题2:一台机器可以创建多少线程? CPU,内存,操作系统,JVM,应用服务器 我们编写一段示例代码,来验证下线程池与非线程池的区别: //线程池和非线程池的区别<font></font>public class ThreadPool {<font></font> <font></font> public static int times = 100;//100,1000,10000<font></font> <font></font> public static ArrayBlockingQueue arrayWorkQueue = new ArrayBlockingQueue(1000);<font></font> public static ExecutorService threadPool = new ThreadPoolExecutor(5, //corePoolSize线程池中核心线程数<font></font> 10,<font></font> 60,<font></font> TimeUnit.SECONDS,<font></font> arrayWorkQueue,<font></font> new ThreadPoolExecutor.DiscardOldestPolicy()<font></font> );<font></font> <font></font> public static void useThreadPool() {<font></font> Long start = System.currentTimeMillis();<font></font> for (int i = 0; i < times; i++) {<font></font> threadPool.execute(new Runnable() {<font></font> public void run() {<font></font> System.out.println("说点什么吧...");<font></font> }<font></font> });<font></font> }<font></font> threadPool.shutdown();<font></font> while (true) {<font></font> if (threadPool.isTerminated()) {<font></font> Long end = System.currentTimeMillis();<font></font> System.out.println(end - start);<font></font> break;<font></font> }<font></font> }<font></font> }<font></font> <font></font> public static void createNewThread() {<font></font> Long start = System.currentTimeMillis();<font></font> for (int i = 0; i < times; i++) {<font></font> <font></font> new Thread() {<font></font> public void run() {<font></font> System.out.println("说点什么吧...");<font></font> }<font></font> }.start();<font></font> }<font></font> Long end = System.currentTimeMillis();<font></font> System.out.println(end - start);<font></font> }<font></font> <font></font> public static void main(String args[]) {<font></font> createNewThread();<font></font> //useThreadPool();<font></font> }<font></font> }启动不同数量的线程,然后比较线程池和非线程池的执行结果: ...

May 10, 2019 · 6 min · jiezi

线程安全可见性

共享变量在线程间不可见的原因线程的交叉执行重排序结合线程交叉执行共享变量更新后的值没有在工作内存与主内存间及时更新使用synchronized的来保证可见性使用synchronized的两条规定: 线程解锁前,必须把共享变量的最新值刷新到主内存线程加锁锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意加锁与解锁是同一把锁)volatile 来实现可见性通过加入内存屏障和禁止重拍讯优化来实现可见性。对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。也就是说使用volatile关键字在读和写操作时都会强迫从主内存中获取变量值。下图是使用volatile写操作的示意图 使用volatile写操作前会插入一条StoreStore指令来禁止在volatile写之前的普通写对volatile写的指令重排序优化,在写之后会插入一条StoreLoad屏障指令来防止上面的volatile写操作和下面可能有的读或者写进行指令重排序。下图是volatile读操作示意图 volatile操作都是cpu指令级别的下面看一段演示代码 @Slf4jpublic class CountExample4 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static volatile int count = 0; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private static void add() { count++; // 1、count // 2、+1 // 3、count }}我们多次运行个这段代码,发现结果并不是我们预期5000,volatile只能保证可见性并不能保证原子性。 ...

May 6, 2019 · 1 min · jiezi

线程安全性原子性

线程安全性定义当多个线程访问同一个类时,不管运行时环境采用何种调度方式,不论线程如何交替执行,在主调代码中不需要额外的协同或者同步代码时,这个类都可以表现出正确的行为,我们则称这个类为线程安全的。线程安全性原子性:提供了互斥访问,同一时刻只能有一个线程来对他进行操作。可见性:一个线程对主内存的修改可以及时被其他线程观察到。有序性:一个线程观察其他线程中的指令顺序,由于指令重排序的存在,该结果一般杂乱无序。原子性 - Atomic包AtomicXXX 是通过 CAS(CompareAndSwap)来保证线程原子性 通过比较操作的对象的值(工作内存的值)与底层的值(共享内存中的值)对比是否相同来判断是否进行处理,如果不相同则重新获取。如此循环操作,直至获取到期望的值。(关于什么是主内存什么事工作内存在上篇博客中进行介绍了,不懂的同学可以翻一下)示例代码: @Slf4jpublic class AtomicExample2 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static AtomicLong count = new AtomicLong(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count.get()); } private static void add() { count.incrementAndGet(); // count.getAndIncrement(); }}LongAdder和DoubleAdderjdk8中新增的保证同步操作的类,我们之前介绍了AtomicXXX来保证原子性,那么为什么还有有LongAdder呢?说AtomicXXX的实现是通过死循环来判断值的,在低并发的情况下AtomicXXX进行更改值的命中率还是很高的。但是在高并发下进行命中率可能没有那么高,从而一直执行循环操作,此时存在一定的性能消耗,在jvm中我们允许将64位的数值拆分成2个32位的数进行储存的,LongAdder的思想就是将热点数据分离,将AtomicXXX中的核心数据分离,热点数据会被分离成多个数组,每个数据都单独维护各自的值,将单点的并行压力发散到了各个节点,这样就提高了并行,在低并发的时候性能基本和AtomicXXX相同,在高并发时具有较好的性能,缺点是在并发更新时统计时可能会出现误差。在低并发,需要全局唯一,准确的比如id等使用AtomicXXX,要求性能使用LongAdder ...

May 5, 2019 · 2 min · jiezi

java并发编程实战学习三

线程封闭 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不要同步。这种技术成为线程封闭(Thread Confinement)。Ad-hoc 线程封闭Ad-hoc 线程封闭是指维护线程封闭的职责完全是由程序自己来承担。栈封闭栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。ThreadLocal类ThreadLocal提供了set和get等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set设置的最新值。import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;/** * ConnectionDispenser * <p/> * Using ThreadLocal to ensure thread confinement * * @author Brian Goetz and Tim Peierls */public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); }}不变性满足同步需求的另一种方法是使用不可变对象。1,对象创建以后其状态就不能修改。2,对象的所有域都是final类型。3,对象是正确创建的。 ...

May 3, 2019 · 1 min · jiezi

java并发编程实战学习二

对象的共享上一章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据,而本章将介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。 列同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键字synchronized只能用于实现原子性或者确定“临界区”。同步还有另一重要的方面;内存可见性。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而希望确保当一个线程修改了对象状态之后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现。3.1 可见性通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。有时甚至是不可能的事情。为了确保多个线程的之间对内存写入操作的可见性,必须使用同步机制。/** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; }}主线程启动读线程,然后将number设为42,并将ready设为true。读线程一直循环知道发现ready的值为true,然后输出number的值。虽然NoVisibility看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。 3.1.1 失效数据查看变脸时,可能会得到一个已经失效的值。下方代码中,如果某线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。 ...

May 3, 2019 · 3 min · jiezi

『并发包入坑指北』之向大佬汇报任务

前言在面试过程中聊到并发相关的内容时,不少面试官都喜欢问这类问题: 当 N 个线程同时完成某项任务时,如何知道他们都已经执行完毕了。这也是本次讨论的话题之一,所以本篇为『并发包入坑指北』的第二篇;来聊聊常见的并发工具。 <!--more--> 自己实现其实这类问题的核心论点都是:如何在一个线程中得知其他线程是否执行完毕。 假设现在有 3 个线程在运行,需要在主线程中得知他们的运行结果;可以分为以下几步: 定义一个计数器为 3。每个线程完成任务后计数减一。一旦计数器减为 0 则通知等待的线程。所以也很容易想到可以利用等待通知机制来实现,和上文的『并发包入坑指北』之阻塞队列的类似。 按照这个思路自定义了一个 MultipleThreadCountDownKit 工具,构造函数如下: 考虑到并发的前提,这个计数器自然需要保证线程安全,所以采用了 AtomicInteger。 所以在初始化时需要根据线程数量来构建对象。 计数器减一当其中一个业务线程完成后需要将这个计数器减一,直到减为0为止。 /** * 线程完成后计数 -1 */ public void countDown(){ if (counter.get() <= 0){ return; } int count = this.counter.decrementAndGet(); if (count < 0){ throw new RuntimeException("concurrent error") ; } if (count == 0){ synchronized (notify){ notify.notify(); } } }利用 counter.decrementAndGet() 来保证多线程的原子性,当减为 0 时则利用等待通知机制来 notify 其他线程。 ...

April 29, 2019 · 2 min · jiezi

ConcurrentHashMap中tabAtsetTabAt方法的意义所在

在学习ConcurrentHashMap时发现,源码中对table数组的元素进行操作时,使用了三个封装好的原子操作方法,如下: /* ---------------- Table element access -------------- *//* * Atomic access methods are used for table elements as well as * elements of in-progress next table while resizing. All uses of * the tab arguments must be null checked by callers. All callers * also paranoically precheck that tab's length is not zero (or an * equivalent check), thus ensuring that any index argument taking * the form of a hash value anded with (length - 1) is a valid * index. Note that, to be correct wrt arbitrary concurrency * errors by users, these checks must operate on local variables, * which accounts for some odd-looking inline assignments below. * Note that calls to setTabAt always occur within locked regions, * and so require only release ordering. */@SuppressWarnings("unchecked")static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);}static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);}casTabAt这个方法我们可以很清晰地明白它封装了对于数组元素的cas操作,但是另外两个方法的意义如何理解呢? ...

April 28, 2019 · 1 min · jiezi

再次认识ReentrantReadWriteLock读写锁

前言最近研究了一下juc包的源码。在研究ReentrantReadWriteLock读写锁的时候,对于其中一些细节的思考和处理以及关于提升效率的设计感到十分的震撼,难以遏制想要分享这份心得的念头,因此在这里写一篇小文章作为记录。 本片文章建立在已经了解并发相关基础概念的基础上,可能不会涉及很多源码,以思路为主。如果文章有什么纰漏或者错误,还请务必指正,预谢。 1. 从零开始考虑如何实现读写锁首先我们需要知道独占锁(RenentractLock)这种基础的锁,在juc中是如何实现的:它基于java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)这个抽象类所搭建的同步框架,利用AQS中的一个volatile int类型变量的CAS操作来表示锁的占用情况,以及一个双向链表的数据结构来存储排队中的线程。 简单地说:一个线程如果想要获取锁,就需要尝试对AQS中的这个volatile int变量(下面简称state)执行类似comapre 0 and swap 1的操作,如果不成功就进入同步队列排队(自旋和重入之类的细节不展开说了)同时休眠自己(LockSupport.park),等待占有锁的线程释放锁时再唤醒它。 那么,如果不考虑重入,state就是一个简单的状态标识:0表示锁未被占用,1表示锁被占用,同步性由volatile和CAS来保证。 上面说的是独占锁,state可以不严谨地认为只有两个状态位。 但是如果是读写锁,那这个锁的基本逻辑应该是:读和读共享、读和写互斥、写和写互斥 如何实现锁的共享呢?如果我们不再把state当做一个状态,而是当做一个计数器,那仿佛就可以说得通了:获取锁时compare n and swap n+1,释放锁时compare n and swap n-1,这样就可以让锁不被独占了。 因此,要实现读写锁,我们可能需要两个锁,一个共享锁(读锁),一个独占锁(写锁),而且这两个锁还需要协作,写锁需要知道读锁的占用情况,读锁需要知道写锁的占用情况。 设想中的简单流程大概如下: 设想中的流程很简单,然而存在一些问题: 关于读写互斥: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?如果允许,会有什么问题?如果不允许,当真的存在这种需求时怎么办?关于写写互斥是否也存在上面两条提到的情况和问题呢?我们知道一般而言,读的操作数量要远远大于写的操作,那么很有可能读锁一旦被获取就长时间处于被占有的情况(因为新来的读操作只需要进去+1就好了,不需要等待state回到0,这样的话state可能永远不会有回到0的一天),这会导致极端情况下写锁根本没有机会上位,该如何解决这种情况?对于上面用计数器来实现共享锁的假设,当任意一个线程想要释放锁(即使它并未获取锁,因为解锁的方法是开放的,任何获取锁对象的线程都可以执行lock.unlock())时,如何判断它是否有权限执行compare n and swap n-1? 是否应该使用ThreadLocal来实现这种权限控制?如果使用ThreadLocal来控制,如何保证性能和效率?2. 带着问题研究ReentrantReadWriteLock在开始研究ReentrantReadWriteLock之前,应当先了解两个概念: 重入性一个很现实的问题是:我们时常需要在锁中加锁。这可能是由代码复用产生的需求,也可能业务的逻辑就是这样。但是不管怎样,在一个线程已经获取锁后,在释放前再次获取锁是一个合理的需求,而且并不生硬。上文在说独占锁时说到如果不考虑重入的情况,state会像boolean一样只有两个状态位。那么如果考虑重入,也很简单,在加锁时将state的值累加即可,表示同一个线程重入此锁的次数,当state归零,即表示释放完毕。 公平、非公平这里的公平和非公平是指线程在获取锁时的机会是否公平。我们知道AQS中有一个FIFO的线程排队队列,那么如果所有线程想要获取锁时都来排队,大家先来后到井然有序,这就是公平;而如果每个线程都不守秩序,选择插队,而且还插队成功了,那这就是不公平。 但是为什么需要不公平呢?因为效率。有两个因素会制约公平机制下的效率: 上下文切换带来的消耗依赖同步队列造成的消耗我们之所以会使用锁、使用并发,可能很大一部分原因是想要挖掘程序的效率,那么相应的,对于性能和效率的影响需要更加敏感。简单地说,上述的两点由于公平带来的性能损耗很可能让你的并发失去高效的初衷。当然这也是和场景密切关联的,比如说你非常需要避免ABA问题,那么公平模式很适合你。具体的不再展开,可以参考这篇文章:深入剖析ReentrantLock公平锁与非公平锁源码实现 回到我们之前提的问题: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?我们先考虑这种情况是否实际存在:假设我们有一个对象,它有两个实例变量a和b,我们需要在实现:if a = 1 then set b = 2,或者换个例子,如果有个用户名字叫张三就给他打钱。 这看上去仿佛是个CAS操作,然而它并不是,因为它涉及了两个变量,CAS操作并不支持这种针对多个变量的疑似CAS的操作。为什么不支持呢?因为cpu不提供这种针对多个变量的CAS操作指令(至少x86不提供),代码层面的CAS只是对cpu指令的封装而已。为什么cpu不支持呢?可以,但没必要鄙人也不是特别清楚(逃)。 总而言之这种情况是存在的,但是在并发情况下如果不加锁就会有问题:比如先判断得到这个用户确实名叫张三,正准备打钱,突然中途有人把他的名字改了,那再打这笔钱就有问题了,我们判断名字和打钱这两个行为中间应当是没有空隙的。那么为了保证这个操作的正确性,我们或许可以在读之前加一个读锁,在我释放锁之前,其他人不得改变任何内容,这就是之前所说的读写互斥:读的期间不准写。但是如果照着这个想法,就产生了自相矛盾的地方:都说了读期间不能写,那你自己怎么在写(打钱)呢? 如果我们顺着这个思路去尝试解释“自己读的期间还在写”的行为的正当性,我们也许可以设立一个规则:如果读锁是我自己持有,则我可以写。然而这会出现其他的问题:因为读锁是共享的,你加了读锁,其他人仍然可以读,这是否会有问题呢?假如我们的打钱操作涉及更多的值的改变,只有这些值全部改变完毕,才能说此时的整体状态正确,否则在改变完毕之前,读到的东西都有可能是错的。再去延伸这个思路似乎会变得非常艰难,也许会陷入耦合的地狱。 但是实际上我们不需要这样做,我们只需要反过来使用读写互斥的概念即可:因为写写互斥(写锁是独占锁),所以我们在执行这个先读后写的行为之前,加一个写锁,这同样能防止其他人来写,同时还可以阻止其他人来读,从而实现我们在单线程中读写并存的需求。 这就是ReentrantReadWriteLock中一个重要的概念:锁降级 对于另一个子问题:如果在已经获取写锁的期间还要再获取写锁的时候怎么办?这种情况还是很常见的,多数是由于代码的复用导致,不过相应的处理也很简单:对写锁这个独占锁增加允许单线程重入的规则即可。 极端情况下写操作根本没有机会上位,该如何解决这种情况?如果我们有两把锁,一把读锁,一把写锁,它们之间想要互通各自加锁的情况很简单——只要去get对方的state就行了。但是只知道state是不够的,对于读的操作来说,它如果只看到写锁没被占用,也不管有多少个写操作还在排队,就去在读锁上+1,那很可能发展成为问题所说的场景:写操作永远没机会上位。 那么我们理想的情况应该是:读操作如果发现写锁空闲,最好再看看写操作的排队情况如何,酌情考虑放弃这一次竞争,让写操作有机会上位。这也是我理解的,为什么ReentrantReadWriteLock不设计成两个互相沟通的、独立的锁,而是公用一个锁(class Sync extends AbstractQueuedSynchronizer)——因为它们看似独立,实际上对于耦合的需求很大,它们不仅需要沟通锁的情况,还要沟通队列的情况。 公用一个锁的具体实现是:使用int state的高16位表示读锁的state,低16位表示写锁的state,而队列公用的方式是给每个节点增加一个标记,表明该节点是一个共享锁的节点(读操作)还是一个独占锁的节点(写操作)。 上面说到的“酌情放弃这一次竞争”,ReentrantReadWriteLock中体现在boolean readerShouldBlock()这个方法里,这个方法有两个模式:公平和非公平,我们来稍微看一点源码先看公平模式的实现: final boolean readerShouldBlock() { return hasQueuedPredecessors();}当线程发现自己可以获取读锁时(写锁未被占用),会调用这个方法,来判断自己是否应该放弃此次获取。hasQueuedPredecessors()这个方法我们不去看源码,因为它的意思很显而易见(实际代码也是):是否存在排队中的线程(Predecessor先驱者可以理解为先来的)。如果有,那就放弃竞争去排队。在公平模式下,无论读写操作,只需要大家都遵守FIFO的秩序,就不会出现问题描述的情况 ...

April 23, 2019 · 3 min · jiezi

使用ConcurrentHashMap一定线程安全?

前言老王为何半夜惨叫?几行代码为何导致服务器爆炸?说好的线程安全为何还是出问题?让我们一起收看今天的《走进IT》 正文CurrentHashMap出现背景说到ConcurrentHashMap的出现背景,还得从HashMap说起。 老王是某公司的苦逼Java开发,在互联网行业中,业务总是迭代得非常快。体现在代码中的话,就是v1.0的模块是单线程执行的,这时候使用HashMap是一个不错的选择。然而到了v1.5的版本,为了性能考虑,老王觉得把这段代码改成多线程会更有效率,那么说改就改,然后就愉快的发布上线了。 直到某天晚上,突然收到线上警报,服务器CPU占用100%。这时候惊醒起来一顿排查(百度,谷歌),结果发现原来是HashMap 在并发的环境下进行rehash的时候会造成链表的闭环,因此在进行get()操作的时候导致了CPU占用100%。喔,原来HashMap不是线程安全的类,在当前的业务场景中会有问题。那么你这时候又想到了Hashtable,没错,这是个线程安全的类,那我先用这个类替换不就行了,一顿commit,push,部署上去了,观察了一段时间,完美~再也没出现过类似的问题了。 但是好日子过的并不长久,运营的同事又找上门了,老王啊,XX功能怎么慢了这么多啊?这时候老王就纳闷了,我没改代码啊?不就上次替换了一个Hashtable,难道这里会有效率的问题?然后又是一顿排查(百度、谷歌),我去,果不其然,原来它线程安全的原因是因为在方法上都加了synchronized,导致我们全部操作都串行化了,难怪这么慢。 经过了2次掉陷阱的经验,这次的老王已经是非常谨慎的去寻求更好的解决方案了,这时他找到ConcurrentHashMap,而且为了避免再次掉坑他也去提前了解了实现原理,原来这个类是使用了Segment分段锁,每一个Segment都有自己的锁,这样冲突的的范围就变小了,效率也能提高不少。经过调研发现确实不错,于是他就放心的把Hashtable给替换掉了,从此运营再也没来吐槽了,老王又过上了幸福的日子。 经过一段时间紧张的业务开发,此时的项目已经去到了v2.0,之前的ConcurrentHashMap相关的代码已经被改的面目全非,逻辑也复杂了很多,但项目还是按时顺利的上线了。在项目在运行了一段时间以后,居然再次出现线程安全的问题,其根源竟然是ConcurrentHashMap,老王叕陷入了沉思... 为何会出问题?抛开复杂的例子,我们用一个多线程并发获取map中的值并加1,看看最后输出的数字如何 public class CHMDemo { public static void main(String[] args) throws InterruptedException { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>(); map.put("key", 1); ExecutorService executorService = Executors.newFixedThreadPool(100); for (int i = 0; i < 1000; i++) { executorService.execute(new Runnable() { @Override public void run() { int key = map.get("key") + 1; //step 1 map.put("key", key);//step 2 } }); } Thread.sleep(3000); //模拟等待执行结束 System.out.println("------" + map.get("key") + "------"); executorService.shutdown(); }}此时我们看看多次执行输出的结果 ...

April 23, 2019 · 1 min · jiezi

并发问题研究

计算机历史At the time of begining,一台计算机只能从头到尾执行一个程序,浪费资源。 后来操作系统出现,--计算机能运行多个程序,一个程序其实就是一个单独的进程。而一个进程有一个或多个线程,提高cpu资源的利用率。

April 22, 2019 · 1 min · jiezi

Java并发编程之CountDownLatch源码解析

一、导语最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程。二、什么是CountDownLatchCountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。 比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。三、简单使用public static void main(String[] args){ System.out.println(“主线程和他的两个小兄弟约好去吃火锅”); System.out.println(“主线程进入了饭店”); System.out.println(“主线程想要开始动筷子吃饭”); //new一个计数器,初始值为2,当计数器为0时,主线程开始执行 CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println(“子线程1——小兄弟A 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程1——小兄弟A 到饭店了”); //一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println(“子线程2——小兄弟B 正在到饭店的路上”); Thread.sleep(3000); System.out.println(“子线程2——小兄弟B 到饭店了”); //另一个小兄弟到了,计数器-1 latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); //主线程等待,直到其他两个小兄弟也进入饭店(计数器==0),主线程才能吃饭 latch.await(); System.out.println(“主线程终于可以开始吃饭了~”);}四、源码分析核心代码:CountDownLatch latch = new CountDownLatch(1); latch.await(); latch.countDown();其中构造函数的参数是计数器的值; await()方法是用来阻塞线程,直到计数器的值为0 countDown()方法是执行计数器-1操作1、首先来看构造函数的代码public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException(“count < 0”); this.sync = new Sync(count); }这段代码很简单,首先if判断传入的count是否<0,如果小于0直接抛异常。 然后new一个类Sync,这个Sync是什么呢?我们一起来看下private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } //尝试获取共享锁 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } //尝试释放共享锁 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。 其中有几个核心点:变量 state是父类AQS里面的变量,在这里的语义是计数器的值getState()方法也是父类AQS里的方法,很简单,就是获取state的值tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写,先有个印象,之后详讲。2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //判断线程是否是中断状态 if (Thread.interrupted()) throw new InterruptedException(); //尝试获取state的值 if (tryAcquireShared(arg) < 0)//step1 doAcquireSharedInterruptibly(arg);//step2 }tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。若state!=0,则返回-1,进入if体内step2处。 下面我们来看acquireSharedInterruptibly(int arg)方法:private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //step1、把当前线程封装为共享类型的Node,加入队列尾部 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //step2、获取当前node的前一个元素 final Node p = node.predecessor(); //step3、如果前一个元素是队首 if (p == head) { //step4、再次调用tryAcquireShared()方法,判断state的值是否为0 int r = tryAcquireShared(arg); //step5、如果state的值==0 if (r >= 0) { //step6、设置当前node为队首,并尝试释放共享锁 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消 if (failed) cancelAcquire(node); } }按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。 1、首先看addWaiter()//step1private Node addWaiter(Node mode) { //把当前线程封装为node Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //获取当前队列的队尾tail,并赋值给pred Node pred = tail; //如果pred!=null,即当前队尾不为null if (pred != null) { //把当前队尾tail,变成当前node的前继节点 node.prev = pred; //cas更新当前node为新的队尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果队尾为空,走enq方法 enq(node);//step1.1 return node; }—————————————————————–//step1.1private Node enq(final Node node) { for (;;) { Node t = tail; //如果队尾tail为null,初始化队列 if (t == null) { // Must initialize //cas设置一个新的空node为队首 if (compareAndSetHead(new Node())) tail = head; } else { //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现//step6private void setHeadAndPropagate(Node node, int propagate) { //获取队首head Node h = head; // Record old head for check below //设置当前node为队首,并取消node所关联的线程 setHead(node); // if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //如果当前node的后继节点为null或者是shared类型的 if (s == null || s.isShared()) //释放锁,唤醒下一个线程 doReleaseShared();//step6.1 } }——————————————————————–//step6.1private void doReleaseShared() { for (;;) { //找到头节点 Node h = head; if (h != null && h != tail) { //获取头节点状态 int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //唤醒head节点的next节点 unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }3、接下来我们来看countDown()方法。public void countDown() { sync.releaseShared(1); }可以看到调用的是父类AQS的releaseShared 方法public final boolean releaseShared(int arg) { //state-1 if (tryReleaseShared(arg)) {//step1 //唤醒等待线程,内部调用的是LockSupport.unpark方法 doReleaseShared();//step2 return true; } return false; }——————————————————————//step1protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { //获取当前state的值 int c = getState(); if (c == 0) return false; int nextc = c-1; //cas操作来进行原子减1 if (compareAndSetState(c, nextc)) return nextc == 0; } }五、总结CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。下面我们通过一个简单的图来帮助我们理解一下:PS:本人也是还在学习的路上,理解的也不是特别透彻,如有错误,愿倾听教诲。^_^ ...

April 18, 2019 · 3 min · jiezi

3分钟干货之什么是并发容器的实现?

何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。 可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

April 13, 2019 · 1 min · jiezi

『并发包入坑指北』之阻塞队列

前言较长一段时间以来我都发现不少开发者对 jdk 中的 J.U.C(java.util.concurrent)也就是 Java 并发包的使用甚少,更别谈对它的理解了;但这却也是我们进阶的必备关卡。之前或多或少也分享过相关内容,但都不成体系;于是便想整理一套与并发包相关的系列文章。其中的内容主要包含以下几个部分:根据定义自己实现一个并发工具。JDK 的标准实现。实践案例。基于这三点我相信大家对这部分内容不至于一问三不知。既然开了一个新坑,就不想做的太差;所以我打算将这个列表下的大部分类都讲到。所以本次重点讨论 ArrayBlockingQueue。自己实现在自己实现之前先搞清楚阻塞队列的几个特点:基本队列特性:先进先出。写入队列空间不可用时会阻塞。获取队列数据时当队列为空时将阻塞。实现队列的方式多种,总的来说就是数组和链表;其实我们只需要搞清楚其中一个即可,不同的特性主要表现为数组和链表的区别。这里的 ArrayBlockingQueue 看名字很明显是由数组实现。我们先根据它这三个特性尝试自己实现试试。初始化队列我这里自定义了一个类:ArrayQueue,它的构造函数如下: public ArrayQueue(int size) { items = new Object[size]; }很明显这里的 items 就是存放数据的数组;在初始化时需要根据大小创建数组。写入队列写入队列比较简单,只需要依次把数据存放到这个数组中即可,如下图:但还是有几个需要注意的点:队列满的时候,写入的线程需要被阻塞。写入过队列的数量大于队列大小时需要从第一个下标开始写。先看第一个队列满的时候,写入的线程需要被阻塞,先来考虑下如何才能使一个线程被阻塞,看起来的表象线程卡住啥事也做不了。有几种方案可以实现这个效果:Thread.sleep(timeout)线程休眠。object.wait() 让线程进入 waiting 状态。当然还有一些 join、LockSupport.part 等不在本次的讨论范围。阻塞队列还有一个非常重要的特性是:当队列空间可用时(取出队列),写入线程需要被唤醒让数据可以写入进去。所以很明显Thread.sleep(timeout)不合适,它在到达超时时间之后便会继续运行;达不到空间可用时才唤醒继续运行这个特点。其实这样的一个特点很容易让我们想到 Java 的等待通知机制来实现线程间通信;更多线程见通信的方案可以参考这里:深入理解线程通信所以我这里的做法是,一旦队列满时就将写入线程调用 object.wait() 进入 waiting 状态,直到空间可用时再进行唤醒。 /** * 队列满时的阻塞锁 / private Object full = new Object(); /* * 队列空时的阻塞锁 */ private Object empty = new Object();所以这里声明了两个对象用于队列满、空情况下的互相通知作用。在写入数据成功后需要使用 empty.notify(),这样的目的是当获取队列为空时,一旦写入数据成功就可以把消费队列的线程唤醒。这里的 wait 和 notify 操作都需要对各自的对象使用 synchronized 方法块,这是因为 wait 和 notify 都需要获取到各自的锁。消费队列上文也提到了:当队列为空时,获取队列的线程需要被阻塞,直到队列中有数据时才被唤醒。代码和写入的非常类似,也很好理解;只是这里的等待、唤醒恰好是相反的,通过下面这张图可以很好理解:总的来说就是:写入队列满时会阻塞直到获取线程消费了队列数据后唤醒写入线程。消费队列空时会阻塞直到写入线程写入了队列数据后唤醒消费线程。测试先来一个基本的测试:单线程的写入和消费。3123123412345通过结果来看没什么问题。当写入的数据超过队列的大小时,就只能消费之后才能接着写入。2019-04-09 16:24:41.040 [Thread-0] INFO c.c.concurrent.ArrayQueueTest - [Thread-0]1232019-04-09 16:24:41.040 [main] INFO c.c.concurrent.ArrayQueueTest - size=32019-04-09 16:24:41.047 [main] INFO c.c.concurrent.ArrayQueueTest - 12342019-04-09 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 123452019-04-09 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 123456从运行结果也能看出只有当消费数据后才能接着往队列里写入数据。而当没有消费时,再往队列里写数据则会导致写入线程被阻塞。并发测试三个线程并发写入300条数据,其中一个线程消费一条。=====0299最终的队列大小为 299,可见线程也是安全的。由于不管是写入还是获取方法里的操作都需要获取锁才能操作,所以整个队列是线程安全的。ArrayBlockingQueue下面来看看 JDK 标准的 ArrayBlockingQueue 的实现,有了上面的基础会更好理解。初始化队列看似要复杂些,但其实逐步拆分后也很好理解:第一步其实和我们自己写的一样,初始化一个队列大小的数组。第二步初始化了一个重入锁,这里其实就和我们之前使用的 synchronized 作用一致的;只是这里在初始化重入锁的时候默认是非公平锁,当然也可以指定为 true 使用公平锁;这样就会按照队列的顺序进行写入和消费。更多关于 ReentrantLock 的使用和原理请参考这里:ReentrantLock 实现原理三四两步则是创建了 notEmpty notFull 这两个条件,他的作用于用法和之前使用的 object.wait/notify 类似。这就是整个初始化的内容,其实和我们自己实现的非常类似。写入队列其实会发现阻塞写入的原理都是差不多的,只是这里使用的是 Lock 来显式获取和释放锁。同时其中的 notFull.await();notEmpty.signal(); 和我们之前使用的 object.wait/notify 的用法和作用也是一样的。当然它还是实现了超时阻塞的 API。也是比较简单,使用了一个具有超时时间的等待方法。消费队列再看消费队列:也是差不多的,一看就懂。而其中的超时 API 也是使用了 notEmpty.awaitNanos(nanos) 来实现超时返回的,就不具体说了。实际案例说了这么多,来看一个队列的实际案例吧。背景是这样的:有一个定时任务会按照一定的间隔时间从数据库中读取一批数据,需要对这些数据做校验同时调用一个远程接口。简单的做法就是由这个定时任务的线程去完成读取数据、消息校验、调用接口等整个全流程;但这样会有一个问题:假设调用外部接口出现了异常、网络不稳导致耗时增加就会造成整个任务的效率降低,因为他都是串行会互相影响。所以我们改进了方案:其实就是一个典型的生产者消费者模型:生产线程从数据库中读取消息丢到队列里。消费线程从队列里获取数据做业务逻辑。这样两个线程就可以通过这个队列来进行解耦,互相不影响,同时这个队列也能起到缓冲的作用。但在使用过程中也有一些小细节值得注意。因为这个外部接口是支持批量执行的,所以在消费线程取出数据后会在内存中做一个累加,一旦达到阈值或者是累计了一个时间段便将这批累计的数据处理掉。但由于开发者的大意,在消费的时候使用的是 queue.take() 这个阻塞的 API;正常运行没啥问题。可一旦原始的数据源,也就是 DB 中没数据了,导致队列里的数据也被消费完后这个消费线程便会被阻塞。这样上一轮积累在内存中的数据便一直没机会使用,直到数据源又有数据了,一旦中间间隔较长时便可能会导致严重的业务异常。所以我们最好是使用 queue.poll(timeout) 这样带超时时间的 api,除非业务上有明确的要求需要阻塞。这个习惯同样适用于其他场景,比如调用 http、rpc 接口等都需要设置合理的超时时间。总结关于 ArrayBlockingQueue 的相关分享便到此结束,接着会继续更新其他并发容器及并发工具。对本文有任何相关问题都可以留言讨论。本文涉及到的所有源码:https://github.com/crossoverJ…你的点赞与分享是对我最大的支持 ...

April 10, 2019 · 1 min · jiezi

PHP并发IO编程之路

并发 IO 问题一直是服务器端编程中的技术难题,从最早的同步阻塞直接 Fork 进程,到 Worker 进程池/线程池,到现在的异步IO、协程。PHP 程序员因为有强大的 LAMP 框架,对这类底层方面的知识知之甚少,本文目的就是详细介绍 PHP 进行并发 IO 编程的各种尝试,最后再介绍 Swoole 的使用,深入浅出全面解析并发 IO 问题。多进程/多线程同步阻塞最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题。进程模型出现的最早,从 Unix 系统诞生就开始有了进程的概念。最早的服务器端程序一般都是 Accept 一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据。多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间是共享内存堆栈的,所以不同的线程之间交互非常容易实现。比如聊天室这样的程序,客户端连接之间可以交互,比聊天室中的玩家可以任意的其他人发消息。用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。而多进程模式就要用到管道、消息队列、共享内存,统称进程间通信(IPC)复杂的技术才能实现。代码实例:多进程/线程模型的流程是创建一个 socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,当然也可以使用更底层的sockets扩展分别实现。进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket主进程在多进程模型下通过fork(php: pcntl_fork)创建子进程,多线程模型下使用pthread_create(php: new Thread)创建子线程。下文如无特殊声明将使用进程同时表示进程/线程。子进程创建成功后进入while循环,阻塞在recv(php: fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用send(php: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close。当客户端连接关闭时,子进程退出并销毁所有资源。主进程会回收掉此子进程。这种模式最大的问题是,进程/线程创建和销毁的开销很大。所以上面的模式没办法应用于非常繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的 Leader-Follower 模型。代码实例:它的特点是程序启动后就会创建N个进程。每个子进程进入 Accept,等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的TCP连接。当此连接关闭时,子进程会释放,重新进入 Accept ,参与处理新的连接。这个模型的优势是完全可以复用进程,没有额外消耗,性能非常好。很多常见的服务器程序都是基于此模型的,比如 Apache 、PHP-FPM。多进程模型也有一些缺点。这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%。另外有一些场景多进程模型无法解决,比如即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的连接(经典的C10K问题),多进程模型就力不从心了。还有一种场景也是多进程模型的软肋。通常Web服务器启动100个进程,如果一个请求消耗100ms,100个进程可以提供1000qps,这样的处理能力还是不错的。但是如果请求内要调用外网Http接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。有没有一种技术可以在一个进程内处理所有并发IO呢?答案是有,这就是IO复用技术。IO复用/事件循环/异步非阻塞其实IO复用的历史和多进程一样长,Linux很早就提供了 select 系统调用,可以在一个进程内维持1024个连接。后来又加入了poll系统调用,poll做了一些改进,解决了 1024 限制的问题,可以维持任意数量的连接。但select/poll还有一个问题就是,它需要循环检测连接是否有事件。这样问题就来了,如果服务器有100万个连接,在某一时间只有一个连接向服务器发送了数据,select/poll需要做循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。直到Linux 2.6内核提供了新的epoll系统调用,可以维持无限数量的连接,而且无需轮询,这才真正解决了 C10K 问题。现在各种高并发异步IO的服务器程序都是基于epoll实现的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 这样单进程单线程的程序,都可以维持超过1百万TCP连接,全部归功于epoll技术。IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化。Reactor有4个核心的操作:add添加socket监听到reactor,可以是listen socket也可以使客户端socket,也可以是管道、eventfd、信号等set修改事件监听,可以设置监听的类型,如可读、可写。可读很好理解,对于listen socket就是有新客户端连接到来了需要accept。对于客户端连接就是收到数据,需要recv。可写事件比较难理解一些。一个SOCKET是有缓存区的,如果要向客户端连接发送2M的数据,一次性是发不出去的,操作系统默认TCP缓存区只有256K。一次性只能发256K,缓存区满了之后send就会返回EAGAIN错误。这时候就要监听可写事件,在纯异步的编程中,必须去监听可写才能保证send操作是完全非阻塞的。del从reactor中移除,不再监听事件callback就是事件发生后对应的处理逻辑,一般在add/set时制定。C语言用函数指针实现,JS可以用匿名函数,PHP可以用匿名函数、对象方法数组、字符串函数名。Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的。具体编码可参考下面的伪代码:Reactor模型还可以与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。目前流行的异步服务器程序都是这样的方式:如Nginx:多进程ReactorNginx+Lua:多进程Reactor+协程Golang:单线程Reactor+多线程协程Swoole:多线程Reactor+多进程Worker协程是什么协程从底层技术角度看实际上还是异步IO Reactor模型,应用层自行实现了任务调度,借助Reactor切换各个当前执行的用户态线程,但用户代码中完全感知不到Reactor的存在。PHP并发IO编程实践PHP相关扩展Stream:PHP内核提供的socket封装Sockets:对底层Socket API的封装Libevent:对libevent库的封装Event:基于Libevent更高级的封装,提供了面向对象接口、定时器、信号处理的支持Pcntl/Posix:多进程、信号、进程管理的支持Pthread:多线程、线程管理、锁的支持PHP还有共享内存、信号量、消息队列的相关扩展PECL:PHP的扩展库,包括系统底层、数据分析、算法、驱动、科学计算、图形等都有。如果PHP标准库中没有找到,可以在PECL寻找想要的功能。PHP语言的优劣势PHP的优点:第一个是简单,PHP比其他任何的语言都要简单,入门的话PHP真的是可以一周就入门。C++有一本书叫做《21天深入学习C++》,其实21天根本不可能学会,甚至可以说C++没有3-5年不可能深入掌握。但是PHP绝对可以7天入门。所以PHP程序员的数量非常多,招聘比其他语言更容易。PHP的功能非常强大,因为PHP官方的标准库和扩展库里提供了做服务器编程能用到的99%的东西。PHP的PECL扩展库里你想要的任何的功能。另外PHP有超过20年的历史,生态圈是非常大的,在Github可以找到很多代码。PHP的缺点:性能比较差,因为毕竟是动态脚本,不适合做密集运算,如果同样的 PHP 程序使用 C/C++ 来写,PHP 版本要比它差一百倍。函数命名规范差,这一点大家都是了解的,PHP更讲究实用性,没有一些规范。一些函数的命名是很混乱的,所以每次你必须去翻PHP的手册。提供的数据结构和函数的接口粒度比较粗。PHP只有一个Array数据结构,底层基于HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等数据结构的功能。另外PHP有一个SPL提供了其他数据结构的类封装。所以PHPPHP更适合偏实际应用层面的程序,业务开发、快速实现的利器PHP不适合开发底层软件使用C/C++、JAVA、Golang等静态编译语言作为PHP的补充,动静结合借助IDE工具实现自动补全、语法提示PHP的Swoole扩展基于上面的扩展使用纯PHP就可以完全实现异步网络服务器和客户端程序。但是想实现一个类似于多IO线程,还是有很多繁琐的编程工作要做,包括如何来管理连接,如何来保证数据的收发原子性,网络协议的处理。另外PHP代码在协议处理部分性能是比较差的,所以我启动了一个新的开源项目Swoole,使用C语言和PHP结合来完成了这项工作。灵活多变的业务模块使用PHP开发效率高,基础的底层和协议处理部分用C语言实现,保证了高性能。它以扩展的方式加载到了PHP中,提供了一个完整的网络通信的框架,然后PHP的代码去写一些业务。它的模型是基于多线程Reactor+多进程Worker,既支持全异步,也支持半异步半同步。Swoole的一些特点:Accept线程,解决Accept性能瓶颈和惊群问题多IO线程,可以更好地利用多核提供了全异步和半同步半异步2种模式处理高并发IO的部分用异步模式复杂的业务逻辑部分用同步模式底层支持了遍历所有连接、互发数据、自动合并拆分数据包、数据发送原子性。Swoole的进程/线程模型:Swoole程序的执行流程:使用PHP+Swoole扩展实现异步通信编程实例代码在https://github.com/swoole/swo… 主页查看。TCP服务器与客户端异步TCP服务器:在这里new swoole_server对象,然后参数传入监听的HOST和PORT,然后设置了3个回调函数,分别是onConnect有新的连接进入、onReceive收到了某一个客户端的数据、onClose某个客户端关闭了连接。最后调用start启动服务器程序。swoole底层会根据当前机器有多少CPU核数,启动对应数量的Reactor线程和Worker进程。异步客户端:客户端的使用方法和服务器类似只是回调事件有4个,onConnect成功连接到服务器,这时可以去发送数据到服务器。onError连接服务器失败。onReceive服务器向客户端连接发送了数据。onClose连接关闭。设置完事件回调后,发起connect到服务器,参数是服务器的IP,PORT和超时时间。同步客户端:同步客户端不需要设置任何事件回调,它没有Reactor监听,是阻塞串行的。等待IO完成才会进入下一步。异步任务:异步任务功能用于在一个纯异步的Server程序中去执行一个耗时的或者阻塞的函数。底层实现使用进程池,任务完成后会触发onFinish,程序中可以得到任务处理的结果。比如一个IM需要广播,如果直接在异步代码中广播可能会影响其他事件的处理。另外文件读写也可以使用异步任务实现,因为文件句柄没办法像socket一样使用Reactor监听。因为文件句柄总是可读的,直接读取文件可能会使服务器程序阻塞,使用异步任务是非常好的选择。异步毫秒定时器这2个接口实现了类似JS的setInterval、setTimeout函数功能,可以设置在n毫秒间隔实现一个函数或 n毫秒后执行一个函数。异步MySQL客户端swoole还提供一个内置连接池的MySQL异步客户端,可以设定最大使用MySQL连接数。并发SQL请求可以复用这些连接,而不是重复创建,这样可以保护MySQL避免连接资源被耗尽。异步Redis客户端异步的Web程序程序的逻辑是从Redis中读取一个数据,然后显示HTML页面。使用ab压测性能如下:同样的逻辑在php-fpm下的性能测试结果如下:WebSocket程序swoole内置了websocket服务器,可以基于此实现Web页面主动推送的功能,比如WebIM。有一个开源项目可以作为参考。https://github.com/matyhtf/ph…

April 8, 2019 · 1 min · jiezi

浅谈并发及Java实现 (一) - 并发设计的三大原则

并发设计的三大原则原子性原子性:对共享变量的操作相对于其他线程是不可干扰的,即其他线程的执行只能在该原子操作完成后或开始前执行。通过一个小例子理解public class Main { private static Integer a = 0; public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) { pool.submit(() -> { a = a + 1; }); } pool.shutdown(); //等待线程全部结束 while(!pool.isTerminated()); System.out.println(a); }}这里创建了一个包含50个线程的线程池,并让每个线程执行一次自增的操作,最后等待全部线程执行结束之后打印a的值。理论上,这个a的值应该是50吧,但实际运行发现并不是如此,而且多次运行的结果不一样。分析一下原因,在多线程的情况下,a = a + 1这一条语句是可能被多个线程同时执行或交替执行的,而这条语句本身分为3个步骤,读取a的值,a的值+1,写回a。假设现在a的值为1,线程A和线程B正在执行。线程A读取a得值为1,并将a得值+1(线程A内a的值目前依旧为1),此时线程B读取a得值为1,将a值+1,写回a,此时a为2,线程A再次运行,将刚才+1后的a值(2)写回a。发现两个线程运行结束后a的值为2。以一个表格描述运行的过程。线程A线程Ba读取a读取a1a + 1a + 1,写回结果2写回结果 2这一现象发生的原因,正是因为a = a + 1其实是由多个步骤所构成的,在一个线程操作的过程中,其他线程也可以进行操作,所以发生了非预期的错误结果。因此,若能保证一个线程在执行操作共享变量的时候,其他线程不能操作,即不能干扰的情况下,就能保证程序正常的运行了,这就是原子性。可见性可见性:当一个线程修改了状态,其他的线程能够看到改变。了解过计算机组成原理的应该知道,为了缓解CPU过高的执行速度和内存过低的读取速度的矛盾,CPU内置了缓存功能,能够存储近期访问过的数据,若需要再次操作这些数据,只需要从缓存中读取即可,大大减少了内存I/O的时间。(此处应当有JVM的内存结构分析,待添加) 但此时就产生了一个问题,在多处理器的情况下,若对同一个内存区域进行操作,就会在多个处理器缓存中存在该内存区域的拷贝。但每个处理器对结果的操作并不能对其他处理器可见,因为各个处理器都在读取自己的缓存区域,这就造成了缓存不一致的情况。同样以一个小例子理解public class Main { private static Boolean ready = false; private static Integer number = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!ready) ; System.out.println(number); }).start(); Thread.sleep(100); number = 42; ready = true; System.out.println(“Main Thread Over !”); }}这里ready初始化为false,创建一个线程,持续监测ready的值,直到为true后打印number的结果。主线程则在创建完线程后给ready和number重新赋值。运行之后发现,程序打印出了Main Thread Over !意味着主线程结束,此时ready和number应该已经被赋值,但等待很久之后发现还是没有正常打印出number的值。因为这里在主线程让线程暂停了一段时间,保证子线程先运行,此时子线程读到的内存中的ready为false,并拷贝至自身的缓存,当主线程运行时,修改了ready的值,而子线程并不知道这一事件的发生,依旧在使用缓冲中的值。这正是因为多线程下缓存的不一致,即可见性问题。如果有兴趣的同学可以将Thread.sleep(100);这句取消,看看结果,分析一下原因。有序性有序性:程序执行的顺序按照代码的先后顺序执行。可能有同学看到这一条不是很理解,而且这个相关的例子也很难给出,因为存在很大的随机性。首先理解一下,为什么会有这一条,难道程序的执行顺序还不是按照我写的代码的顺序吗?其实还真不一定是。上面讲到,每个处理器都会有一个高速缓存,在程序运行中,更多次数的命中缓存,往往意味着更高效率的运行,而缓存的空间实际是很小的,可能时常需要让出空间为新变量使用。针对这一点,很多编译器内置了一个优化,通过不影响程序的运行结果,调整部分代码的位置,使得高速缓存的利用率提升。例如Integer a,b;a = a + 1; //(1)b = b - 3; //(2)a = a + 1; //(3)如果处理器的缓存空间很小,只能存下一个变量,那么将第(3)句放置(1),(2)句之间,是不是缓存多使用了一次,而且没有改变程序的运行结果。这就是重排序问题,当然重排序提升的不仅仅是缓存利用率,还有其他很多的方面。到这里,可能会有疑问,不是说保证不影响程序运行结果才会有重排序发生吗,为什么还要考虑这一点。重排序遵守一个happens-before原则,而这个原则实则并没有对多线程交替的情况进行考虑,因为这太复杂,考虑多线程的交替性还要进行重排序而不影响运行结果的最好办法,就是不排序 :-)happens-before原则同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。Thread.start()的调用会happens-before于启动线程里面的动作。Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。一个对象构造函数的结束happens-before与该对象的finalizer的开始如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。那么,多线程下的重排序会怎么样影响程序的结果呢?还是拿上一个例子来讲public class Main { private static volatile Boolean ready = false; private static volatile Integer number = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!ready) ; System.out.println(number); }).start(); number = 42; //(1) ready = true; //(2) System.out.println(“Main Thread Over !”); }}注意此处删除了线程休眠的代码。这里我们假设理想的情况,现在整个程序已经满足了可见性(具体怎么实现见后文),而此时发生了重排序,将(1)(2)两行的内容进行了交换,子线程开始了运行,并持续检测ready中。主线程执行,由于发生了重排序,(2)将先会执行,此时子线程看到ready变为了true,之后打印出number的值,此时,number的值为0,而预期的结果应该是42。这就是在多线程情况下要求程序执行的顺序按照代码的先后顺序执行的原因之一。 ...

March 31, 2019 · 1 min · jiezi

【J2SE】java并发编程实战 读书笔记( 一、二、三章)

线程的优缺点线程是系统调度的基本单位。线程如果使用得当,可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。多线程程序可以通过提高处理器资源的利用率来提升系统的吞吐率。与此同时,在线程的使用开发过程中,也存在着诸多需要考虑的风险。安全性:有合理的同步下,多线程的并发随机执行使线程安全性变得复杂,如++i。活跃性:在多线程中,常因为缺少资源而处于阻塞状态,当某个操作不幸造成无限循环,无法继续执行下去的时候,就会发生活跃性问题。性能:线程总会带来程序的运行时开销,多线程中,当频繁地出现上下文切换操作时,将会带来极大的开销。线程安全性线程安全的问题着重于解决如何对状态访问操作进行管理,特别是对共享和可变的状态。共享意味着可多个线程同时访问;可变即在变量在其生命周期内可以被改变;状态就是由某个类中的成员变量(Field)。一个无状态的对象一定是线程安全的。因为它没有可被改变的东西。public class LoginServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { System.out.println(“无状态Servlet,安全的类,没有字段可操作”); }}原子性正如我们熟知的 ++i操作,它包含了三个独立的“读取-修改-写入”操作序列,显然是一个复合操作。为此java提供了原子变量来解决 ++i这类问题。当状态只是一个的时候,完全可以胜任所有的情况,但当一个对象拥有两个及以上的状态时,仍然存在着需要思考的复合操作,尽管状态都使用原子变量。如下:public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } }} // lastNumber lastFactors 虽然都是原子的,但是 if-else 是复合操作,属“先验条件” 既然是复合操作,最直接,简单的方式就是使用synchronized将这个方法同步起来。这种方式能到达预期效果,但效率十分低下。既然提到synchronized加锁同步,那么就必须知道 锁的特点:锁是可以重入的。即子类的同步方法可以调用本类或父类的同步方法。同一时刻,只有一个线程能够访问对象中的同步方法。静态方法的锁是 类;普通方法的锁是 对象本身。回顾上面的代码,一个方法体中,只要涉及了多个状态的时候,就一定需要同步整个方法吗?答案是否定的,同步只是为了让多步操作为原子性,即对复合操作同步即可,因此需要明确的便是哪些操作是复合操作。如下:public class CachedFactorizer implements Servlet { private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = 1; lastFactors = factors.clone(); } } encodeIntoResponse(reqsp, factors); }}// 两个synchronized分别同步独立的复合操作。对象共享重排序:当一个线程修改对象状态后,其他线程没有看见修改后的状态,这种现象称为“重排序”。java内存模型允许编译器对操作顺序进行重排序,并将数据缓存在寄存器中。当缺乏同步的情况下,每一个线程在独立的缓存中使用缓存的数据,并不知道主存中的数据已被更改。这就涉及到内存可见性的问题。可见性内存可见性:同步的另一个重要的方面。我们不仅希望防止多个线程同时操作对象状态,而且还希望确保某一个线程修改了状态后,能被其他线程看见变化。volatile:使用 synchronized可以实现内存可见,但java提供了一种稍弱的更轻量级得同步机制volatile变量。在访问volatile变量时不会执行加锁操作,因此不会产生线程阻塞。即便如此还是不能过度使用volatile,当且仅当能简化代码的实现以及对同步策略的验证时,才考虑使用它。发布与逸出发布指:使对象能够在当前作用于之外的代码中使用。即对象引用能被其他对象持有。发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。逸出指:当某个不应该发布的对象被发布时,这种情况被称为逸出。// 正确发布:对象引用放置公有静态域中,所有类和线程都可见class CarFactory { public static Set<Car> cars; private CarFactory() { cars = new HashSet<Car>(); } // 私有,外部无法获取 CarFactory的引用 public static Car void newInstance() { Car car = new Car(“碰碰车”); cars.put(car); return car; } // 使用方法来获取 car}// 逸出class Person { private String[] foods = new String[] {“土豆”}; public Person(Event event) { person.registListener { new EventListener() { public void onEvent(Event e) { doSomething(e); } } } }// 隐式逸出了this,外界得到了Person的引用 并且 EventListener也获取了Person的引用。 public String[] getFoods() { return foods; }// 对发布的私有 foods,外界还是可以修改foods内部值}线程封闭将可变的数据仅放置在单线程中操作的技术,称之为发线程封闭。栈封闭:只能通过局部变量才能访问对象。局部变量的固有属性之一就是封装在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈,即只在一个方法内创建和使用对象。public int test(Person p) { int num = 0; PersonHolder holder = new PersonHolder(); Person newPerson = deepCopy(p); Person woman = holder.getLove(newPerson); newPerson.setWomen(person); num++; return num; // 基本类型没有引用,对象创建和修改都没有逸出本方法}ThreadLocal类:ThreadLocal能够使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了 get、set等访问接口的方法,这些方法为每一个使用该变量的线程都存有一份独立的副本,因get总是返回由当前执行线程在调用set时设置的最新值。private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } };public static Connection getConnection() { return connectionHolder.get();}当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用ThreadLocal。不变性线程安全性是不可变对象的固有属性之一。不可变对象一定是线程安全的,它们的不变性条件是由构造函数创建的,只要它们的状态不可变。// 在可变对象基础上构建不可变类public final class ThreadStooges { private final Set<String> stooges = new HashSet<String>(); public ThreadStooges() { stooges.add(“Moe”); stooges.add(“Larry”); } public boolean isStooge(String name) { return stooges.contains(name); }}// 没有提供可修改状态的方式,尽管使用了Set可变集合,但被private final修饰着对象不可变的条件对象创建以后其状态就不能修改。对象的所有域都是final类型。对象是正确创建的(在对象的创建期间,this引用没有逸出)安全发布任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。// 安全的 Holder类class Holder { private int n; public Holder(int n) { this.n = n; }}public class SessionHolder { // 错误的发布,导致 Holder不安全 public Holder holder; public void init() { holder = new Holder(10); }}// 当初始化 holder的时候,holder.n会被先默认初始化为 0,然后构造函数才初始化为 10;在并发情况下,可能会有线程在默认初始化 与 构造初始化中,获取到 n 值为 0, 而不是 10;要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式安全发布:在静态初始化函数中初始化一个对象引用。将对象的引用保存到 volatitle 类型的域或者 AtomicReferance 对象中。将对象的引用保存到某个正确构造对象的 final 类型域中。将对象的引用保存到一个由锁保护的域中。在线程并发容器中的安全发布:通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。通过将某个元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,可以将元素安全地发布到任何从这些容器中访问该元素的线程。通过将某个元素放入 BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。通常,要发布一个静态构造的对象,最简单、安全的方式就是使用静态的初始化器。如public static Holder holder = new Holder(10)。如果对象在发布后状态不会被修改(则称为事实不可变对象),那么在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的不可变对象。对象的发布需求取决于它的可变性:不可变对象可以通过任意机制来发布。事实不可变对象必须通过安全方式来发布。可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁保护起来。在并发程序中使用和共享对象时可采用的策略:线程封闭。将对象封闭在线程中,如在方法中创建和修改局部对象。只读共享。线程安全共享。对象内部实现同步,使用公有接口来访问。保护对象。使用特定的锁来保护对象。 ...

March 19, 2019 · 2 min · jiezi

【J2SE】java并发基础

并发简述并发通常是用于提高运行在单处理器上的程序的性能。在单 CPU 机器上使用多任务的程序在任意时刻只在执行一项工作。并发编程使得一个程序可以被划分为多个分离的、独立的任务。一个线程就是在进程中的一个单一的顺序控制流。java的线程机制是抢占式。线程的好处是提供了轻量级的执行上下文切换,只改变了程序的执行序列和局部变量。多线程的主要缺陷:<!– java编程思想 –>等待共享资源的时候性能降低。需要处理线程的额外 CPU 花费。糟糕的程序设计导致不必要的复杂度。有可能产生一些病态行为,若饿死、竞争、死锁和活锁。不同平台导致的不一样。volatile关键字源来:当程序运行,JVM会为每一个线程分配一个独立的缓存用于提高执行效率,每一个线程都在自己独立的缓存中操作各自的数据。一个线程在缓冲中对数据进行修改,写入到主存后,其他线程无法得知数据已被更改,仍在操作缓存中已过时的数据,为了解决这个问题,提供了volatile关键字,实现内存可见,一旦主存数据被修改,便致使其他线程缓存数据行无效,强制前往主存获取新数据。Example:内存不可见,导致主线程无法结束。class ThreadDemo implements Runnable { //添加volatile关键字可实现内存可见性 public volatile boolean flag = false; public boolean flag = Boolean.false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = Boolean.true; System.out.println(“ThreadDemo over”); } public boolean isFlag() { return flag; }}public class TestVolatile { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); new Thread(demo).start(); while (true) { if (demo.flag || demo.isFlag()) { System.out.println(“Main over”); break; } } }}/output:打印ThreadDemo over,主线程持续循环/作用:当多个线程操作共享数据时,保证内存中的数据可见性。采用底层的内存栅栏,及时的将缓存中修改的数据刷新到主存中,并导致其他线程所缓存的数据无效,使得这些线程必须去主存中获取修改的数据。优缺点:保证内存可见性,让各个线程能够彼此获取最新的内存数据。较传统synchronized加锁操作提高了效率,若有线程正在操作被synchronized修饰的代码块数据时,其他线程试图进行操作,发现已被其他线程占用,试图操作的线程必须挂起,等到下一次继续尝试操作。对volatile修饰的数据被修改后,其他线程必须前往主存中读取,若修改频繁,需要不断读取主存数据,效率将会降低。使用volatile,底层采用内存栅栏,JVM将不会对其提供指令重排序及其优化。不具备互斥性。多个线程可以同时对数据进行操作,只是由原来的在缓存操作转变成了直接在主存中操作。(synchronized是互斥的,一个线程正在执行,其他线程必须挂起等待)不保证变量的原子性。使用volatile仅仅是一个能保证可见性的轻量级同步策略。原子变量与 CAS 算法Example:使用volatile修饰,number自增问题。class ThreadDemo implements Runnable { public volatile int number = 0; @Override public void run() { try { Thread.sleep(200); } catch (Exception e) { } System.out.print(getIncrementNumber() + " “); } public int getIncrementNumber() { return ++number; }}public class TestAtomic { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); for (int i = 0; i < 10; i++) { new Thread(demo).start(); } }}/*output: 1 5 4 7 3 9 2 1 8 6 /// ++number底层原理思想int temp = number; // ①number = number + 1; // ②temp = number; // ③return temp; // ④由 ++number 可知,返回的是 temp 中存储的值,且自增是一个多步操作,当多个线程调用 incrementNumber方法时,方法去主存中获取 number 值放入 temp 中,根据 CPU 时间片切换,当 A 线程完成了 ③ 操作时,时间片到了被中断,A 线程开始执行 ① 时不幸被中断,接着 A 获取到了CPU执行权,继续执行完成 ④ 操作更新了主存中的值,紧接着 B 线程开始执行,但是 B 线程 temp中存储的值已经过时了。注意:自增操作为四步,只有在第四步的时候才会刷新主存的值,而不是number = number + 1 操作就反映到主存中去。如图所示:源来:volatile只能保证内存可见性,对多步操作的变量,无法保证其原子性,为了解决这个问题,提供了原子变量。作用:原子变量既含有volatile的内存可见性,又提供了对变量原子性操作的支持,采用底层硬件对并发操作共享数据的 CAS(Compare-And-Swap)算法,保证数据的原子性。提供的原子类:类描述AtomicBoolean一个 boolean值可以用原子更新。AtomicInteger可能原子更新的 int值。AtomicIntegerArray一个 int数组,其中元素可以原子更新。AtomicIntegerFieldUpdater<T>基于反射的实用程序,可以对指定类的指定的 volatile int字段进行原子更新。AtomicLong一个 long值可以用原子更新。AtomicLongArray可以 long地更新元素的 long数组。AtomicLongFieldUpdater<T>基于反射的实用程序,可以对指定类的指定的 volatile long字段进行原子更新。AtomicMarkableReference<V>AtomicMarkableReference维护一个对象引用以及可以原子更新的标记位。AtomicReference<V>可以原子更新的对象引用。AtomicReferenceArray<E>可以以原子方式更新元素的对象引用数组。AtomicReferenceFieldUpdater<T,V>一种基于反射的实用程序,可以对指定类的指定的 volatile volatile引用原子更新。AtomicStampedReference<V>AtomicStampedReference维护对象引用以及可以原子更新的整数“印记”。DoubleAccumulator一个或多个变量一起维护使用提供的功能更新的运行的值 double 。DoubleAdder一个或多个变量一起保持初始为零 double和。LongAccumulator一个或多个变量,它们一起保持运行 long使用所提供的功能更新值。LongAdder一个或多个变量一起保持初始为零 long总和。CAS算法:CAS(Compare-And-Swap)是底层硬件对于原子操作的一种算法,其包含了三个操作数:内存值(V),预估值(A),更新值(B)。当且仅当 V == A 时, 执行 V = B 操作;否则不执行任何结果。这里需要注意,A 和 B 两个操作数是原子性的,同一时刻只能有一个线程进行AB操作。优缺点:操作失败时,直接放弃结果,并不释放对CPU的控制权,进而可以继续尝试操作,不必挂起等待。(synchronized会让出CPU)当多个线程并发的对主存中的数据进行操作时,有且只有一个会成功,其余均失败。原子变量中封装了用于对数据的原子操作,简化了代码的编写。Collection并发类HashMap 与 HashTable简述HashMap是线程不安全的,而HashTable是线程安全的,因为HashTable所维护的Hash表存在着独占锁,当多个线程并发访问时,只能有一个线程可进行操作,但是对于复合操作时,HashTable仍然存在线程安全问题,不使用HashTable的主要原因还是效率低下。// 功能:不包含obj,则添加if (!hashTable.contains(obj)) { // 复合操作,执行此处时线程中断,obj被其他线程添加至容器中,此处继续执行将导致重复添加 hashTable.put(obj);}可知上述两个操作需要 “原子性”,为了达到效果,还不是得对代码块进行同步ConcurrentHashMap采用锁分段机制,分为 16 个段(并发级别),每一个段下有一张表,该表采用链表结构链接着各个元素,每个段都使用独立的锁。当多个线程并发操作的时候,根据各自的级别不同,操作不同的段,多个线程并行操作,明显提高了效率,其次还提供了复合操作的诸多方法。注:jdk1.8由原来的数组+单向链表结构转换成数据+单向链表+红黑树结构。ConcurrentSkipListMap和ConcurrentSkipListSet有序的哈希表,通过跳表实现,不允许null作为键或值。ConcurrentSkipListMap详解CopyOnWriteArrayList 和 CopyOnWriteArraySet对collection进行写入操作时,将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成时,一个原子性的操作将把心的数组换人,使得新的读取操作可以看到新的修改。<!–Java编程思想–>好处之一是当多个迭代器同时遍历和修改列表时,不会抛出ConcurrentModificationException。小结:当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMapConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。并发迭代操作多时,可选择CopyOnWriteArrayList 和 CopyOnWriteArraySet。高并发情况下,可选择ConcurrentSkipListMap和ConcurrentSkipListSetCountDownLatch闭锁源由:当一个修房子的 A 线程正在执行,需要砖头时,开启了一个线程 B 去拉砖头,此时 A 线程需要等待 B 线程的结果后才能继续执行时,但是线程之间都是并行操作的,为了解决这个问题,提供了CountDownLatch。作用:一个同步辅助类,为了保证执行某些操作时,“所有准备事项都已就绪”,仅当某些操作执行完毕后,才能执行后续的代码块,否则一直等待。CountDownLatch中存在一个锁计数器,如果锁计数器不为 0 的话,它会阻塞任何一个调用 await() 方法的线程。也就是说,当一个线程调用 await() 方法时,如果锁计数器不等于 0,那么就会一直等待锁计数器为 0 的那一刻,这样就解决了需要等待其他线程执行完毕才执行的需求。Example:class ThreadDemo implements Runnable { private CountDownLatch latch = null; public ThreadDemo(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { System.out.println(“execute over”); } finally { latch.countDown(); // 必须保证计数器减一 } }}public class TestCountDownLatch { public static void main(String[] args) { final int count = 10; final CountDownLatch latch = new CountDownLatch(count); ThreadDemo demo = new ThreadDemo(latch); for (int i = 0; i < count; ++i) { new Thread(demo).start(); } try { latch.await(); // 等待计数器为 0 System.out.println(“其他线程结束,继续往下执行…”); } catch (InterruptedException e) { e.printStackTrace(); } }}/**output: execute over … 其他线程结束,继续往下执行…/细节:子线程完毕后,必须调用 countDown() 方法使得 锁计数器减一,否则将会导致调用 await() 方法的线程持续等待,尽可能的放置在 finally 中。锁计数器的个数与子线程数最好相等,只要计数器等于 0,不论是否还存在子线程,await() 方法将得到响应,继续执行后续代码。Callable接口源由:当开启一个线程执行运算时,可能会需要该线程的计算结果,之前的 implements Runnable 和 extends Thread 的 run() 方法并没有提供可以返回的功能,因此提供了 Callable接口。 Callable 的运行结果, 需要使用 FutureTask 类来接受。Example:class ThreadDemo implements Callable<Integer> { private Integer cycleValue; public ThreadDemo(Integer cycleValue) { this.cycleValue = cycleValue; } @Override public Integer call() throws Exception { int result = 0; for (int i=0; i<cycleValue; ++i) { result += i; } return result; } }public class TestCallable { public static void main(String[] args) throws Exception { ThreadDemo demo = new ThreadDemo(Integer.MAX_VALUE); // 使用FutureTask接受结果 FutureTask<Integer> task = new FutureTask<>(demo); new Thread(task).start(); Integer result = task.get(); // 等待计算结果返回, 闭锁 System.out.println(result); }}/output:1073741825 /Lock同步锁和Condition线程通信控制对象Lock:在进行性能测试时,使用Lock通常会比使用synchronized要高效许多,并且synchronized的开销变化范围很大,而Lock相对稳定。只有在性能调优时才使用Lock对象。<!–Java编程思想–>Condition: 替代了 Object 监视器方法的使用,描述了可能会与锁有关的条件标量,相比 Object 的 notifyAll() ,Condition 的 signalAll() 更安全。Condition 实质上被绑定到一个锁上,使用newCondition() 方法为 Lock 实例获取 Condition。Lock和Condition对象只有在困难的多线程问题中才是必须的。<!–Java编程思想–>synchonized与Lock的区别:synchonizedLock隐式锁显示锁JVM底层实现,由JVM维护由程序员手动维护 灵活控制(也有风险)“虚假唤醒”:当一个线程A在等待时,被另一个线程唤醒,被唤醒的线程不一定满足了可继续向下执行的条件,如果被唤醒的线程未满足条件,而又向下执行了,那么称这个现象为 “虚假唤醒”。// 安全的方式,保证退出等待循环前,一定能满足条件while (条件) { wait();}Example:生产消费者<!–参考Java编程思想 P712–>// 产品carclass Car { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean available = false; // false:无货;true有货 public void put(){ lock.lock(); try { while (available) { // 有货等待 condition.await(); } System.out.println(Thread.currentThread().getName() + “put(): 进货”); available = true; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void get() { lock.lock(); try { while (!available) { // 无货等待 condition.await(); } System.out.println(Thread.currentThread().getName() + “get():出货”); available = false; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }}// 消费者class Consume implements Runnable { private Car car; public Consume(Car car) { this.car = car; } @Override public void run() { for (int i=0; i<TestProduceAndConsume.LOOP_SIZE; ++i) { car.get(); try { Thread.sleep(100); } catch (InterruptedException e) { } } } }// 生产者class Produce implements Runnable { private Car car; public Produce(Car car) { this.car = car; } @Override public void run() { for (int i=0; i<TestProduceAndConsume.LOOP_SIZE; i++) { car.put(); } } }public class TestProduceAndConsume { public static final int LOOP_SIZE = 10; public static void main(String[] args) { Car car = new Car(); for (int i=0; i<5; ++i) { Consume consume = new Consume(car); Produce produce = new Produce(car); new Thread(consume, i + “–”).start(); new Thread(produce, i + “–”).start(); } } }每一个 对lock()的调用都必须紧跟着一个 try-finally 子句,用以保证可以在任何情况下都能释放锁,任务在调用 await()、signal()、signalAll()之前,必须拥有锁。lock.lock();try { … // 业务代码} finally { lock.unlock();}ReadWriteLock读写锁源由:上述讲解的锁都是读写一把锁,不论是读或写,都是一把锁解决,当多线程访问数据时,若发生了一千次操作,其中的写操作只执行了一次,数据的更新率非常低,那么每次进行读操作时,都要加锁读取”不会更改的“数据,显然是不必要的开销,因此出现了 ReadWriteLock 读写锁,该对象提供读锁和写锁。作用:ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 write写入操作,那么多个线程可以同时进行持有读锁。而写入锁是独占的,当执行写操作时,其他线程不可写,也不可读。性能的提升取决于读写操作期间读取数据相对于修改数据的频率,如果读取操作远远大于写入操作时,便能增强并发性。Example:class Demo { private int value = 0; private ReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " : " + value); } finally { lock.readLock().unlock(); } } public void write(int value) { lock.writeLock().lock(); try { this.value = value; System.out.println(“write(” + value + “)”); } finally { lock.writeLock().unlock(); } }}class ReadLock implements Runnable { private Demo demo = null; public ReadLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<20; ++i) { demo.read(); try { Thread.sleep(320); } catch (InterruptedException e) { } } } }class WriteLock implements Runnable { private Demo demo = null; public WriteLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<10; ++i) { demo.write(i); try { Thread.sleep(200); } catch (InterruptedException e) { } } } }public class TestReadWriteLock { public static void main(String[] args) { Demo demo = new Demo(); ReadLock readLock = new ReadLock(demo); WriteLock writeLock = new WriteLock(demo); for (int i=0; i<3; ++i) { new Thread(readLock, i + “–”).start(); } new Thread(writeLock).start(); }}/**output: 0– : 0 1– : 0 2– : 0 write(0) write(1) 1– : 1 2– : 1 0– : 1 write(2) write(3) 1– : 3 0– : 3 …/线程池与线程调度源来:在传统操作中(如连接数据库),当我们需要使用一个线程的时候,就 直接创建一个线程,线程完毕后被垃圾收集器回收。每一次需要线程的时候,不断的创建与销毁,大大增加了资源的开销。作用:线程池维护着一个线程队列,该队列中保存着所有等待着的线程,避免了重复的创建与销毁而带来的开销。体系结构:Execuotr:负责线程的使用与调度的根接口。 |- ExecutorService:线程池的主要接口。 |- ForkJoinPool:采用分而治之技术将任务分解。 |- ThreadPoolExecutor:线程池的实现类。 |- ScheduledExecutorService:负责线程调度的子接口。 |- ScheduledThreadPoolExecutor:负责线程池的调度。继承ThreadPoolExecutor并实现ScheduledExecutorService接口Executors 工具类API描述:方法描述ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定数量的无界队列线程池。使用了有限的线程集来执行所提交的所有任务。创建的时候可以一次性预先进行代价高昂的线程分配。ExecutorService newWorkStealingPool(int parallelism)创建一个维护足够的线程以支持给定的parallelism并行级别的线程池。ExecutorService newSingleThreadExecutor()创建一个使用单个线程运行的无界队列的执行程序。ExecutorService newCachedThreadPool()创建一个根据需要创建新线程的线程池,当有可用线程时将重新使用以前构造的线程。ScheduledExecutorService newSingleThreadScheduledExecutor()创建一个单线程执行器,可以调度命令在给定的延迟之后运行,或定期执行。ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。ThreadFactory privilegedThreadFactory()返回一个用于创建与当前线程具有相同权限的新线程的线程工厂。补充:ExecutorService.shutdown():防止新任务被提交,并继续运行被调用之前所提交的所有任务,待任务都完成后退出。CachedThreadPoo在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,是Executor的首选。仅当这个出现问题时,才需切换 FixedThreadPool。SingleThreadExecutor: 类似于线程数量为 1 的FixedThreadPool,但它提供了不会存在两个及以上的线程被并发调用的并发。Example:线程池public class TestThreadPool { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; ++i) { Future<String> future = pool.submit(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName(); } }); String threadName = future.get(); System.out.println(threadName); } pool.shutdown(); // 拒绝新任务并等待正在执行的线程完成当前任务后关闭。 }}/**output: pool-1-thread-1 pool-1-thread-2 pool-1-thread-1 pool-1-thread-2 …/Example:线程调度public class TestThreadPool { public static void main(String[] args) throws Exception { ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); for (int i = 0; i < 5; ++i) { ScheduledFuture<String> future = pool.schedule(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName() + " : " + Instant.now(); } }, 1, TimeUnit.SECONDS); // 延迟执行单位为 1秒的任务 String result = future.get(); System.out.println(result); } pool.shutdown(); }}/*output: pool-1-thread-1 : 2019-03-18T12:10:31.260Z pool-1-thread-1 : 2019-03-18T12:10:32.381Z pool-1-thread-2 : 2019-03-18T12:10:33.382Z pool-1-thread-1 : 2019-03-18T12:10:34.383Z pool-1-thread-2 : 2019-03-18T12:10:35.387Z/<span style=“color: red”>注意:若没有执行 shutdown()方法,则线程会一直等待而不停止。</span>ForkJoinPool分支/合并框架源由:在一个线程队列中,假如队头的线程由于某种原因导致了阻塞,那么在该队列中的后继线程需要等待队头线程结束,只要队头一直阻塞,这个队列中的所有线程都将等待。此时,可能其他线程队列都已经完成了任务而空闲,这种情况下,就大大减少了吞吐量。ForkJoin的“工作窃取”模式:当执行一个新任务时,采用分而治之的思想,将其分解成更小的任务执行,并将分解的任务加入到线程队列中,当某一个线程队列没有任务时,会随机从其他线程队列中“偷取”一个任务,放入自己的队列中执行。Example:// 求次方: value为底,size为次方数class CountPower extends RecursiveTask<Long> { private static final long serialVersionUID = 1L; public Long value = 0L; public int size = 0; public static final Long CRITICAL = 10L; // 阈值 public CountPower(Long value, int size) { this.value = value; this.size = size; } @Override protected Long compute() { // 当要开方的此时 小于 阈值,则计算 (视为最小的任务单元) if(size <= CRITICAL) { Long sum = 1L; for (int i=0; i<size; ++i) { sum *= value; } return sum; } else { int mid = size / 2; // 拆分任务,并压入线程队列 CountPower leftPower = new CountPower(value, mid); leftPower.fork(); CountPower rightPower = new CountPower(value, size - mid); rightPower.fork(); // 将当前两个任务返回的执行结果再相乘 return leftPower.join() * rightPower.join(); } } }public class TestForkJoinPool { public static void main(String[] args) throws Exception { ForkJoinPool pool = new ForkJoinPool(); CountPower task = new CountPower(2L, 11); Long result = pool.invoke(task); System.out.println(result); }}/*output: 2048/根据分而治之的思想进行分解,需要一个结束递归的条件,该条件内的代码就是被分解的最小单元。使用fork()在当前任务正在运行的池中异步执行此任务,即将该任务压入线程队列。调用join()`返回计算结果。RecursiveTask是有返回值的task,RecursiveAction则是没有返回值的。参考尚硅谷JUC视频教程《java编程思想》第 21 章 并发 ...

March 19, 2019 · 6 min · jiezi

[Java并发]1,入门:并发编程Bug的源头

介绍如何坚决并发问题,首先要理解并发的实际源头怎么发生的。现代大家使用的计算机的不同硬件的运行速度是不一样的,这个大家应该都是知道的。计算机数据传输运行速度上的快慢比较: CPU > 缓存 > I/O如何最大化的让不通速度的硬件可以更好的协调执行,需要做一些“撮合”的工作CUP增加了高速缓存来均衡与缓存间的速度差异操作系统增加了 进程,线程,以分时复用CPU,进而均衡CPU与I/O的速度差异(当等待I/O的时候切换给其他CPU去执行)现代编程语言的编译器优化指令顺序,使得缓存能够合理的利用上面说来并发才生问题的背景,下面说下并发才生的具体原因是什么缓存导致的可见性问题先看下单核CPU和缓存之间的关系:单核情况下,也是最简单的情况,线程A操作写入变量A,这个变量A的值肯定是被线程B所见的。因为2个线程是在一个CPU上操作,所用的也是同一个CPU缓存。这里我们来定义一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为 “可见性”多核CPU时代下,我们在来看下具体情况:很明显,多核情况下每个CPU都有自己的高速缓存,所以变量A的在每个CPU中可能是不同步的,不一致的。结果程A刚好操作来CPU1的缓存,而线程B也刚好只操作了CPU2的缓存。所以这情况下,当线程A操作变量A的时候,变量并不对线程B可见。我们用一段经典的代码说明下可见性的问题: private void add10K() { int idx = 0; while (idx++ < 100000) { count += 1; } } @Test public void demo() { // 创建两个线程,执行 add() 操作 Thread th1 = new Thread(() -> { add10K(); }); Thread th2 = new Thread(() -> { add10K(); }); // 启动两个线程 th1.start(); th2.start(); // 等待两个线程执行结束 try { th1.join(); th2.join(); } catch (Exception exc) { exc.printStackTrace(); } System.out.println(count); }大家应该都知道,答案肯定不是 200000这就是可见性导致的问题,因为2个线程读取变量count时,读取的都是自己CPU下的高速缓存内的缓存值,+1时也是在自己的高速缓存中。线程切换带来的原子性问题进程切换最早是为了提高CPU的使用率而出现的。比如,50毫米操作系统会重新选择一个进程来执行(任务切换),50毫米成为“时间片”早期的操作系统是进程间的切换,进程间的内存空间是不共享的,切换需要切换内存映射地址,切换成本大。而一个进程创建的所有线程,内存空间都是共享的。所以现在的操作系统都是基于更轻量的线程实现切换的,现在我们提到的“任务切换”都是线程切换。任务切换的时机大多数在“时间片”结束的时候。现在我们使用的基本都是高级语言,高级语言的一句对应多条CPU命令,比如 count +=1 至少对应3条CPU命令,指令:1, 从内存加载到CPU的寄存器2, 在寄存器执行 +13, 最后,讲结果写回内存(缓存机制导致可能写入的是CPU缓存而不是内存)操作系统做任务切换,会在 任意一条CPU指令执行完就行切换。所以会导致问题如图所示,线程A当执行完初始化count=0时候,刚好被线程切换给了线程B。线程B执行count+1=1并最后写入值到缓存中,CPU切换回线程A后,继续执行A线程的count+1=1并再次写入缓存,最后缓存中的count还是为1.一开始我们任务count+1=1应该是一个不能再被拆开的原子操作。我们把一个或多个操作在CPU执行过程中的不被中断的特性称为 原子性。CPU能够保证的原子性,是CPU指令级别的。所以高级语言需要语言层面 保证操作的原子性。编译优化带来的有序性问题有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:a=6;b=7;编译器优化后可能变成b=7;a=6;,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。Java中的经典案例,双重检查创建单例对象;public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }}看似完美的代码,其实有问题。问题就在new上。想象中 new操作步骤:1,分配一块内存 M2,在内存M上 初始化对象3,把内存M地址赋值给 变量实际上就行编译后的顺序是:1,分开一块内存 M2,把内存M地址赋值给 变量3,在 内存M上 初始化对象优化导致的问题:如图所示,当线程A执行到第二步的时候,被线程切换了,这时候,instance未初始化实例的对象,而线程B这时候执行到instance == null ?的判断中,发现instance已经有“值”了,导致了返回了一个空对象的异常。总结1,缓存引发的可见性2,切换线程带来的原子性3,编译带来的有序性深刻理解这些前因后果,可以诊断大部分并发的问题! ...

March 18, 2019 · 1 min · jiezi

Go语言高阶:调度器系列(1)起源

如果把语言比喻为武侠小说中的武功,如果只是会用,也就是达到四五层,如果用的熟练也就六七层,如果能见招拆招也得八九层,如果你出神入化,立于不败之地十层。如果你想真正掌握一门语言的,怎么也得八层以上,需要你深入了解这门语言方方面面的细节。希望以后对Go语言的掌握能有八九层,怎么能不懂调度器!?Google、百度、微信搜索了许多Go语言调度的文章,这些文章上来就讲调度器是什么样的,它由哪些组成,它的运作原理,搞的我只能从这些零散的文章中形成调度器的“概貌”,这是我想要的结果,但这还不够。学习不仅要知其然,还要知其所以然。学习之前,先学知识点的历史,再学知识,这样你就明白,为什么它是当下这个样子。所以,我打算写一个goroutine调度器的系列文章,从历史背景讲起,循序渐进,希望大家能对goroutine调度器有一个全面的认识。这篇文章介绍调度器相关的历史背景,请慢慢翻阅。远古时代上面这个大家伙是ENIAC,它诞生在宾夕法尼亚大学,是世界第一台真正的通用计算机,和现代的计算机相比,它是相当的“笨重”,它的计算能力,跟现代人手普及的智能手机相比,简直是一个天上一个地下,ENIAC在地下,智能手机在天上。它上面没有操作系统,更别提进程、线程和协程了。进程时代后来,现代化的计算机有了操作系统,每个程序都是一个进程,但是操作系统在一段时间只能运行一个进程,直到这个进程运行完,才能运行下一个进程,这个时期可以成为单进程时代——串行时代。和ENIAC相比,单进程是有了几万倍的提度,但依然是太慢了,比如进程要读数据阻塞了,CPU就在哪浪费着,伟大的程序员们就想了,不能浪费啊,怎么才能充分的利用CPU呢?后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。线程时代多进程真实个好东西,有了对进程的调度能力之后,伟大的程序员又发现,进程拥有太多资源,在创建、切换和销毁的时候,都会占用很长的时间,CPU虽然利用起来了,但CPU有很大的一部分都被用来进行进程调度了,怎么才能提高CPU的利用率呢?大家希望能有一种轻量级的进程,调度不怎么花时间,这样CPU就有更多的时间用在执行任务上。后来,操作系统支持了线程,线程在进程里面,线程运行所需要资源比进程少多了,跟进程比起来,切换简直是“不算事”。一个进程可以有多个线程,CPU在执行调度的时候切换的是线程,如果下一个线程也是当前进程的,就只有线程切换,“很快”就能完成,如果下一个线程不是当前的进程,就需要切换进程,这就得费点时间了。这个时代,CPU的调度切换的是进程和线程。多线程看起来很美好,但实际多线程编程却像一坨屎,一是由于线程的设计本身有点复杂,而是由于需要考虑很多底层细节,比如锁和冲突检测。协程多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存(每个线程的内存占用级别为MB),线程多了之后调度也会消耗大量的CPU。伟大的程序员们有开始想了,如何才能充分利用CPU、内存等资源的情况下,实现更高的并发?既然线程的资源占用、调度在高并发的情况下,依然是比较大的,是否有一种东西,更加轻量?你可能知道:线程分为内核态线程和用户态线程,用户态线程需要绑定内核态线程,CPU并不能感知用户态线程的存在,它只知道它在运行1个线程,这个线程实际是内核态线程。用户态线程实际有个名字叫协程(co-routine),为了容易区分,我们使用协程指用户态线程,使用线程指内核态线程。协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。协程和线程有3种映射关系:N:1,N个协程绑定1个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1个进程的所有协程都绑定在1个线程上,一是某个程序用不了硬件的多核加速能力,二是一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。1:1,1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点,但有一个缺点是协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。M:N,M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂。协程是个好东西,不少语言支持了协程,比如:Lua、Erlang、Java(C++即将支持),就算语言不支持,也有库支持协程,比如C语言的coroutine(风云大牛作品)、Kotlin的kotlinx.coroutines、Python的gevent。goroutineGo语言的诞生就是为了支持高并发,有2个支持高并发的模型:CSP和Actor。鉴于Occam和Erlang都选用了CSP(来自Go FAQ),并且效果不错,Go也选了CSP,但与前两者不同的是,Go把channel作为头等公民。就像前面说的多线程编程太不友好了,Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。Go中,协程被称为goroutine(Rob Pike说goroutine不是协程,因为他们并不完全相同),它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。Go语言的老调度器终于来到了Go语言的调度器环节。调度器的任务是在用户态完成goroutine的调度,而调度器的实现好坏,对并发实际有很大的影响,并且Go的调度器就是M:N类型的,实现起来也是最复杂。现在的Go语言调度器是2012年重新设计的(设计方案),在这之前的调度器称为老调度器,老调度器的实现不太好,存在性能问题,所以用了4年左右就被替换掉了,老调度器大概是下面这个样子:最下面是操作系统,中间是runtime,runtime在Go中很重要,许多程序运行时的工作都由runtime完成,调度器就是runtime的一部分,虚线圈出来的为调度器,它有两个重要组成:M,代表线程,它要运行goroutine。Global G Queue,是全局goroutine队列,所有的goroutine都保存在这个队列中,goroutine用G进行代表。M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。老调度器有4个缺点:创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M’。M中的mcache是用来存放小对象的,mcache和栈都和M关联造成了大量的内存开销和差的局部性。系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统开销。Go语言的新调度器面对以上老调度的问题,Go设计了新的调度器,设计文稿:https://golang.org/s/go11sched新调度器引入了:P:Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M’那里偷取G。现在,调度器中3个重要的缩写你都接触到了,所有文章都用这几个缩写,请牢记:G: goroutineM: 工作线程P: 处理器,它包含了运行Go代码的资源,M必须和一个P关联才能运行G。这篇文章的目的不是介绍调度器的实现,而是调度器的一些理念,帮助你后面更好理解调度器的实现,所以我们回归到调度器设计思想上。调度器的有两大思想:复用线程:协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。调度器的两小策略:抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。上面提到并行了,关于并发和并行再说一下:Go创始人Rob Pike一直在强调go是并发,不是并行,因为Go做的是在一段时间内完成几十万、甚至几百万的工作,而不是同一时间同时在做大量的工作。并发可以利用并行提高效率,调度器是有并行设计的。并行依赖多核技术,每个核上在某个时间只能执行一个线程,当我们的CPU有8个核时,我们能同时执行8个线程,这就是并行。结束语这篇文章的主要目的是为后面介绍Go语言调度器做铺垫,由远及近的方式简要介绍了多进程、多线程、协程、并发和并行有关的“史料”,希望你了解为什么Go采用了goroutine,又为何调度器如此重要。如果你等不急了,想了解Go调度器相关的原理,看下这些文章:设计方案:https://golang.org/s/go11sched代码中关于调度器的描述:https://golang.org/src/runtim…引用最多的调度器文章:https://morsmachine.dk/go-sch…kavya的PPT,目前看到的讲调度最好的PPT:https://speakerdeck.com/kavya…work stealing论文:http://supertech.csail.mit.ed…分析调度器的论文(就问你6不6,还有论文研究):http://www.cs.columbia.edu/~a…声明:关于老调度器的资料已经完全搜不到,根据新版调度器设计方案的描述,想象着写了老调度器这一章,可能存在错误。参考资料https://en.wikipedia.org/wiki…https://en.wikipedia.org/wiki...https://en.wikipedia.org/wiki...https://golang.org/doc/faq#go...https://golang.org/s/go11schedhttps://golang.org/src/runtim…如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/03/10/golang-scheduler-1-history

March 10, 2019 · 1 min · jiezi

当我们在说“并发、多线程”,说的是什么?

这篇文章的目的并不是想教你如何造火箭(面试造火箭,工作拧螺丝),而是想通过对原理和应用案例的有限度剖析来协助你构建起并发的思维,并将操作系统的理论知识与工程实践结合起来,贯穿从学到会的全过程。当然,虽然我们是从实用角度出发,但具有实践意义的深层次知识点永远会是面试中的杀手锏,这可比只能口头造火箭的理论知识更吸引面试官。本文适合谁:希望能了解并发概念的初学者需要理清并发概念与技术的工程师对并发在工作中的应用与其底层实现原理感兴趣的读者在这篇文章中,你将了解到并发与多线程相关的一系列概念,通过一些例子我们可以在不纠结于具体的技术细节的情况下形成对并发与多线程相关的各种概念的抽象理解。有了这些概念以后,我们再去学习具体的理论和技术细节就是手到擒来的事了。什么是并发?最近几年淘宝发展得如火如荼,涌现出了一大批白手起家的卖家。想象一下你是一个刚刚起步的小卖家,自己运营一个服装网店,每天都要自己打包发货。刚开始时生意一般,每天自己一个人一个小时就能干完。随着生意的蓬勃发展,发货时间慢慢地从一个小时涨到了两个小时、四个小时,一次因为延迟发货导致被投诉之后,你终于觉得该招更多的人了。很快,两个小伙伴加入了你的事业,打包速度开始有了质的提高。这就是最基本的并发了,每个人都可以看成是一个线程,同样的工作量,干得人多了自然就快了。所以并发就是通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。数据竞争但是好景不长,周末一盘货,你发现少了不少。这办公室里也没遭贼,怎么就会少货呢?细细一查快递单,你发现竟然有几单发重了。之后的几天你都细细留意了一下发货的过程,最后发现是因为每个人都会拿着一张发货清单去备货,如果有一些订单不小心打印重复了,就有可能会被不同的人重复发货。虽然数量不多,但是也很心痛啊。这个问题产生的原因就是因为每个人在备货之前拿到的订单状态(未发货)在实际备货时发生了变化(已由其他人发货)。这种对一个共享数据(订单的发货状态)本应独占的读取、检查、修改过程,如果发生了并发,这种情况就被称为数据竞争。而这个读取、检查、修改的过程就被称为临界区,临界区指的就是一个存在数据竞争的代码片段。数据竞争出现的根本原因是一个数据本来应该只能由一个执行器完整地执行读取、检查、修改过程,但是如果出现了并发,那么就没办法保证到了“修改”这一步时的数据还保持了“读取”时的值了。确定原因后,有人想到了一个好办法,可以打印一张总的发货清单,这样所有人都必须以这个清单上的订单是否发货来确定是否要对订单进行备货并发货了。因为清单只有一份,所以每次只能由一个人来修改订单的发货状态。这种只能由一个执行器进行数据修改操作来避免发生数据竞争问题的做法就被称为互斥,也就是我们常说的锁了。分布式并发概念分布式因为你管理得当,生意发展得很快,现在的办公室里已经堆不下所有衣服了。所以你又租了一个仓库来同样进行发货。两个地方都会进行发货,那么就可以把每一个仓库理解为一台独立的计算机,这样通过多台计算机完成同一任务的方式就可以被称为分布式,这样的一组计算机的集合就被称为集群。这时候之前用一张纸质的总发货清单的数据竞争解决方式就行不通了,所以我们需要把这张总发货清单放到云端,让大家可以通过网络进行编辑,但是每次只能一个人编辑。在这种情况下,我们可以把两个仓库各自看成一台计算机/进程,而每个仓库里的人就是这个进程中的线程。这样的话这张总发货清单就成为了一个分布式锁,因为它每次只能有一个人编辑,所以是一个互斥锁,或者简称为锁;而因为它可以被两个进程/计算机(仓库)共同使用,所以被称为是分布式锁。什么是进程/线程?可以简单地将进程理解为我们电脑/手机上的一个应用,同一台手机上的每个App都是一个进程,同一个App在每个手机上也是一个进程。进程和进程之间可以理解为是两个仓库,互相之间物理隔离;而线程就是仓库里的每一个人,他们共享同一个办公空间。这里的办公空间就可以理解为操作系统中的虚拟内存空间,但是本文主要讨论并发相关的概念,就不继续展开了。分布式数据不一致因为生意比较好,所以所有人都很忙。有时候就会因为有一些人虽然在云端表格上已经勾上了一个订单,但是一忙就给忙忘了。其他人怕重复发货又不会再去处理已经勾上的订单了,因为这样导致的未发货订单让店铺被投诉了好多次,影响非常大。这种在并发过程中修改了数据状态但是没有完成后续执行的情况就会出现数据不一致,即订单已经被勾上,但实际并没有发货。但是作为聪明的老板,你又想到了解决的方法。每隔一小时两个仓库就会各派一个人检查一下已经勾上的订单是否已经都打包完贴上快递单了。这种每隔一段时间就检查并处理遗漏的数据不一致订单的任务就被称为兜底任务。而通过兜底任务实现的在最后所有订单都会达到数据一致状态的情况就被称为最终一致性。优化方式大家可能早就觉得前面介绍的总发货清单的方法太傻了,只要每个订单都只打印一张发货清单,由单独一个人去负责分发清单就可以了,其他人只要处理好自己被分配到的订单就可以了。最后再加上一个兜底任务对订单的发货情况进行二次校验基本上就不会发生漏发或者重发的情况了。这种由一个执行器进行任务拆分,再由一组执行器进行处理,最后再由一个或一组执行器进行结果汇总的处理方式就是现在非常流行的map-reduce方法了。这种方法在大数据或者程序语言标准库里都有大量的应用,比如大数据领域赫赫有名的Hadoop和Java语言中的ForkJoinPool都使用了这种思想。回顾在这篇文章中,我们涉及到了以下的技术名词:并发,通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。数据竞争,对一个共享数据本应独占的读取、检查、修改过程发生了并发的情况。临界区,存在数据竞争的代码片段。互斥锁(也可以简称为“锁”),同一时间只能由一个执行器获取的实体,用于实现对临界区的互斥(只有一个)访问。分布式,通过多台计算机完成同一任务的方式。集群,一组完成同一任务的机器。分布式锁,在不同机器/进程上提供互斥能力的锁。数据不一致,一系列操作不具有原子性,一部分执行成功而另一部分没有,导致不同数据之间存在矛盾,例如订单已经是发货状态,但是实际没有发货。兜底任务,处理数据不一致状态的任务。最终一致性,通过兜底任务或其他方式保证数据不一致的情况最终会消失。map-reduce,一种任务拆分-执行-再合并的任务执行方式,可以有效地利用多台机器、多核CPU的性能。后记因为并发的知识范围很大,而且对于一些抽象概念的传递必然会需要花费一些篇幅,所以这个主题将会包含一系列文章,主要覆盖以下主题:什么是并发?抛开冗长繁杂的技术点,直接理解并发相关的各种概念。什么是多线程?多线程是并发的一种重要形式。通过具体的多线程问题引出多线程编程中的关键点和对应的工具与知识点,轻松学会多线程编程。常用工具中的并发实现通过解析知名开源工具中的并发方案实现来深入理解并发编程实践。有兴趣的读者可以继续关注后续的文章,在之后的文章中会有对并发编程、操作系统原语、硬件原语等等理论与实践知识的详细介绍与案例。对数据库索引感兴趣的读者可以了解一下我之前的文章:数据库索引是什么?新华字典来帮你 —— 理解数据库索引融会贯通 —— 深入20分钟数据库索引设计实战 —— 实战数据库索引为什么用B+树实现? —— 扩展

March 5, 2019 · 1 min · jiezi

Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节将介绍项目中Redis的引入。Redis下载教程。若对Redis感兴趣,还可以看一下我的另一篇文章造个轮子 | 自己动手写一个Redis存在问题:数据来源于第三方的接口,依赖性太强。可能带来的不良结果:(1)延时性:用户访问我们的时候,我们需要再去访问第三方的接口,我们是数据的中间者,不是数据的产生者,有一定的延时性。(2)访问上限:免费的接口,可能会达到上限。(3)调死:可能将对方的接口给调死。解决方案:使用redis缓存系统,提高整体的并发访问能力。Redis 是一个高性能的key-value数据库,基于内存的缓存系统,对内存的操作时非常快的,所以可以做到及时响应。为什么选择Redis(1)及时响应(2)减少服务调用Redis如何引入Redis是一个key-value结构的数据存储系统,这里我们使用天气数据的uri作为它的key,通过ValueOperations<String, String>ops对象的set方法将数据写入缓存中,通过其get方法可以从缓存中获取数据,并且使用TIME_OUT设置缓存失效的时间。我们并不是每次都去调用第三方的接口,若Redis缓存中有要查找的天气数据,则从缓存中取;若缓存中没有,则请求第三方接口,然后将数据写入Redis缓存中。 private WeatherResponse doGetWeahter(String uri) { String key = uri; String strBody = null; ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); // 先查缓存,缓存有的取缓存中的数据 if (stringRedisTemplate.hasKey(key)) { logger.info(“Redis has data”); strBody = ops.get(key); } else { logger.info(“Redis don’t has data”); // 缓存没有,再调用服务接口来获取 ResponseEntity<String> respString = restTemplate.getForEntity(uri, String.class); if (respString.getStatusCodeValue() == 200) { strBody = respString.getBody(); } // 数据写入缓存 ops.set(key, strBody, TIME_OUT, TimeUnit.SECONDS); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { //e.printStackTrace(); logger.error(“Error!",e); } return resp; } ...

February 27, 2019 · 1 min · jiezi

Java多线程001——一图读懂线程与进程

本博客 猫叔的博客,转载请申明出处前言本系列将由浅入深,学习Java并发多线程。一图读懂线程与进程1、一个进程可以包含一个或多个线程。(其实你经常听到“多线程”,没有听过“多进程”嘛)2、进程存在堆和方法区3、线程存在程序计数器和栈4、堆占最大内存,其为创建时分配的,是多线程共享的,主要存放new创建的对象5、方法区也是多线程共享的,主要存放类、常量、静态变量6、CPU的基本执行单位是线程(注意!不是进程)7、由此,线程需要一个程序计数器记录当前线程要执行的指令地址8、当CPU的时间片用完,让出后记录当前执行地址,下次继续执行(时间片轮询)9、只有执行Java代码时pc技数器记录的才是下一条指令的地址,执行native方法,则记录的是undefined地址10、线程中的栈,只要存储线程局部变量、调用栈帧栈帧:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

February 19, 2019 · 1 min · jiezi

【Java并发】线程安全性

线程安全性定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。线程安全性主要体现在三个方面:原子性、可见性、有序性:原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作可见性:一个线程对主内存的修改可以及时地被其他线程观察到有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序原子性原子性在 JDK 中主要由两个方面体现出来:Atomic一个是 JDK 中已经提供好的 Atomic 包,它们均使用了 CAS 完成线程的原子性操作(详见【Java并发】浅析 AtomicLong & LongAdder)。另一个是使用锁的机制来处理线程之间的原子性。锁主要包括:synchronized、lock。synchronized依赖于 JVM 去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。synchronized 是 Java 中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:修饰代码块:大括号括起来的代码,作用于调用的对象修饰方法:整个方法,作用于调用的对象修饰静态方法:整个静态方法,作用于所有对象修饰类:括号括起来的部分,作用于所有对象注意:如果当前类是一个父类,子类调用父类的被 synchronized 修饰的方法,不会携带 synchronized 属性,因为 synchronized 不属于方法声明的一部分。Lock首先要说明的就是 Lock,通过查看 Lock 的源码可知,Lock 是一个接口。ReentrantLock 是唯一实现了 Lock 接口的类,意思是“可重入锁”,并且 ReentrantLock 提供了更多的方法。public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition();}锁的分类可重入锁synchronized / ReentrantLock可中断锁synchronized 不可中断,Lock 可中断公平锁synchronized 非公平锁ReentrantLock 和 ReentrantReadWriteLock 默认情况下非公平锁,可设置为公平锁读写锁ReadWriteLock / ReentrantReadWriteLock可见性导致共享变量在线程间不可见的原因:线程交叉执行重排序结合线程交叉执行共享变量更新后的值没有在工作内存与主存间及时更新JVM 对于可见性,提供了 synchronized 和 volatile:synchronizedJMM 关于 synchronized 的两条规定:线程解锁前,必须把共享变量的最新值刷新到主内存线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)volatilevolatile 的方式是:通过加入内存屏障和禁止重排序优化来实现。对 volatile 变量写操作时,会在写操作后加入一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存。对 volatile 变量读操作时,会在读操作前加入一条 load 屏障指令,从主内存中读取共享变量。volatile的屏障操作都是 cpu 级别的;适合状态验证,不适合累加值,volatile关键字不具有原子性。适合状态验证,不适合累加值,volatile关键字不具有原子性有序性Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。而 Java 提供了 volatile、synchronized、Lock,它们可以用来保证有序性。另外,Java 内存模型具备一些先天的有序性,即不需要任何手段就能得到保证的有序性。通常被我们称为happens-before 原则(先行发生原则)。如果两个线程的执行顺序无法从 happens-before 原则推导出来,那么就不能保证它们的有序性,虚拟机就可以对它们进行重排序。【以下规则摘抄自《深入理解Java虚拟机》】程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作(重要)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行思维导图笔记整理自:【IMOOC】Java并发编程与高并发解决方案 ...

February 16, 2019 · 1 min · jiezi

Go嵌套并发实现EDM,附坑点分析#1

看着身边优秀的小伙伴们早就开始写博客,自己深感落后,还好迟做总比不做好,勉励自己见贤思齐。趁着年前最后一个周末,阳光正好,写下第一篇博客,为2019年开个头,以期完成今年为自己立下的flags。从PHPer转Gopher,很大一个原因就是业务对性能和并发的持续需求,另一个主要原因就是Go语言原生的并发特性,可以在提供同等高可用的能力下,使用更少的机器资源,节约可观的成本。因此本文就结合自己在学习Go并发的实战demo中,把遇到的一些坑点写下来,共享进步。1. 在Go语言中实现并发控制,目前主要有三种方式:a) Channel - 分为无缓冲、有缓冲通道;b) WaitGroup - sync包提供的goroutine间的同步机制;c) Context - 在调用链不同goroutine间传递和共享数据;本文demo中主要用到了前两种,基本使用请查看官方文档。2. Demo需求与分析:需求:实现一个EDM的高效邮件发送:需要支持多个国家(可以看成是多个任务),需要记录每条任务发送的状态(当前成功、失败条数),需要支持可暂停(stop)、重新发送(run)操作。分析:从需求可以看出,在邮件发送中可以通过并发实现多个国家(多个任务)并发、单个任务分批次并发实现快速、高效EDM需求。3. Demo实战源码:3.1 main.gopackage mainimport ( “bufio” “fmt” “io” “log” “os” “strconv” “sync” “time”)var ( batchLength = 20 wg sync.WaitGroup finish = make(chan bool))func main() { startTime := time.Now().UnixNano() for i := 1; i <= 3; i++ { filename := “./task/edm” + strconv.Itoa(i) + “.txt” start := 60 go RunTask(filename, start, batchLength) } // main 阻塞等待goroutine执行完成 fmt.Println(<-finish) fmt.Println(“finished all tasks.”) endTime := time.Now().UnixNano() fmt.Println(“Total cost(ms):”, (endTime-startTime)/1e6)}// 单任务func RunTask(filename string, start, length int) (retErr error) { for { readLine, err := ReadLines(filename, start, length) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { fmt.Println(err) retErr = err break } fmt.Println(“current line:”, readLine) start += length // 等待一批完成才进入下一批 //wg.Wait() } wg.Wait() finish <- true return retErr}注意上面wg.Wait()的位置(下面有讨论),在finish channel之前,目的是为了等待子goroutine运行完,再通过一个无缓冲通道finish通知main goroutine,然后main运行结束。func ReadLines()读取指定行数据:// 读取指定行数据func ReadLines(filename string, start, length int) (line int, retErr error) { fmt.Println(“current file:”, filename) fileObj, err := os.Open(filename) if err != nil { panic(err) } defer fileObj.Close() // 跳过开始行之前的行-ReadString方式 startLine := 1 endLine := start + length reader := bufio.NewReader(fileObj) for { line, err := reader.ReadString(byte(’\n’)) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { log.Fatal(err) retErr = err break } if startLine > start && startLine <= endLine { wg.Add(1) // go并发执行 go SendEmail(line) if startLine == endLine { break } } startLine++ } return startLine, retErr}// 模拟邮件发送func SendEmail(email string) error { defer wg.Done() time.Sleep(time.Second * 1) fmt.Println(email) return nil}运行上面main.go,3个任务在1s内并发完成所有邮件(./task/edm1.txt中一行表示一个邮箱)发送。truefinished all tasks.Total cost(ms): 1001那么问题来了:没有实现分批每次并发batchLength = 20,因为如果不分批发送,只要其中某个任务或某一封邮件出错了,那下次重新run的时候,会不知道哪些用户已经发送过了,出现重复发送。而分批发送即使中途出错了,下一次重新run可从上次出错的end行开始,最多是[start - end]一个batchLength 发送失败,可以接受。于是,将倒数第5行wg.Wait()注释掉,倒数第8行注释打开,如下:// 单任务func RunTask(filename string, start, length int) (retErr error) { for { readLine, err := ReadLines(filename, start, length) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { fmt.Println(err) retErr = err break } fmt.Println(“current line:”, readLine) start += length // 等待一批完成才进入下一批 wg.Wait() } //wg.Wait() finish <- true return retErr}运行就报错:panic: sync: WaitGroup is reused before previous Wait has returned提示WaitGroup在goroutine之间重用了,虽然是全局变量,看起来是使用不当。怎么调整呢?3.2 main.gopackage mainimport ( “bufio” “fmt” “io” “log” “os” “strconv” “sync” “time”)var ( batchLength = 10 outerWg sync.WaitGroup)func main() { startTime := time.Now().UnixNano() for i := 1; i <= 3; i++ { filename := “./task/edm” + strconv.Itoa(i) + “.txt” start := 60 outerWg.Add(1) go RunTask(filename, start, batchLength) } // main 阻塞等待goroutine执行完成 outerWg.Wait() fmt.Println(“finished all tasks.”) endTime := time.Now().UnixNano() fmt.Println(“Total cost(ms):”, (endTime-startTime)/1e6)}// 单任务func RunTask(filename string, start, length int) (retErr error) { for { isFinish := make(chan bool) readLine, err := ReadLines(filename, start, length, isFinish) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { fmt.Println(err) retErr = err break } // 等待一批完成才进入下一批 fmt.Println(“current line:”, readLine) start += length <-isFinish // 关闭channel,释放资源 close(isFinish) } outerWg.Done() return retErr}从上面可以看出:调整的思路是外层用WaitGroup控制,里层用channel 控制,执行又报错 : (fatal error: all goroutines are asleep - deadlock!goroutine 1 [semacquire]:sync.runtime_Semacquire(0x55fe7c) /usr/local/go/src/runtime/sema.go:56 +0x39sync.(*WaitGroup).Wait(0x55fe70) /usr/local/go/src/sync/waitgroup.go:131 +0x72main.main() /home/work/data/www/docker_env/www/go/src/WWW/edm/main.go:31 +0x1abgoroutine 5 [chan send]:main.ReadLines(0xc42001c0c0, 0xf, 0x3c, 0xa, 0xc42008e000, 0x0, 0x0, 0x0)仔细检查,发现上面代码中定义的isFinish 是一个无缓冲channel,在发邮件SendMail() 子协程没有完成时,读取一个无数据的无缓冲通道将阻塞当前goroutine,其他goroutine也是一样的都被阻塞,这样就出现了all goroutines are asleep - deadlock!于是将上面代码改为有缓冲继续尝试:isFinish := make(chan bool, 1)// 读取指定行数据func ReadLines(filename string, start, length int, isFinish chan bool) (line int, retErr error) { fmt.Println(“current file:”, filename) // 控制每一批发完再下一批 var wg sync.WaitGroup fileObj, err := os.Open(filename) if err != nil { panic(err) } defer fileObj.Close() // 跳过开始行之前的行-ReadString方式 startLine := 1 endLine := start + length reader := bufio.NewReader(fileObj) for { line, err := reader.ReadString(byte(’\n’)) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { log.Fatal(err) retErr = err break } if startLine > start && startLine <= endLine { wg.Add(1) // go并发执行 go SendEmail(line, wg) if startLine == endLine { isFinish <- true break } } startLine++ } wg.Wait() return startLine, retErr}// 模拟邮件发送func SendEmail(email string, wg sync.WaitGroup) error { defer wg.Done() time.Sleep(time.Second * 1) fmt.Println(email) return nil}运行,又报错了 : (fatal error: all goroutines are asleep - deadlock!goroutine 1 [semacquire]:sync.runtime_Semacquire(0x55fe7c) /usr/local/go/src/runtime/sema.go:56 +0x39sync.(*WaitGroup).Wait(0x55fe70)这次提示有点不一样,看起来是里层的WaitGroup 导致了死锁,继续检查发现里层wg 是值传递,应该使用指针传引用。// go并发执行go SendEmail(line, wg)最后修改代码如下:// 读取指定行数据func ReadLines(filename string, start, length int, isFinish chan bool) (line int, retErr error) { fmt.Println(“current file:”, filename) // 控制每一批发完再下一批 var wg sync.WaitGroup fileObj, err := os.Open(filename) if err != nil { panic(err) } defer fileObj.Close() // 跳过开始行之前的行-ReadString方式 startLine := 1 endLine := start + length reader := bufio.NewReader(fileObj) for { line, err := reader.ReadString(byte(’\n’)) if err == io.EOF { fmt.Println(“Read EOF:”, filename) retErr = err break } if err != nil { log.Fatal(err) retErr = err break } if startLine > start && startLine <= endLine { wg.Add(1) // go并发执行 go SendEmail(line, &wg) if startLine == endLine { isFinish <- true break } } startLine++ } wg.Wait() return startLine, retErr}// 模拟邮件发送func SendEmail(email string, wg *sync.WaitGroup) error { defer wg.Done() time.Sleep(time.Second * 1) fmt.Println(email) return nil}赶紧运行一下,这次终于成功啦 : )current line: 100current file: ./task/edm2.txtRead EOF: ./task/edm2.txtRead EOF: ./task/edm2.txtfinished all tasks.Total cost(ms): 4003每个任务模拟的是100行,从第60行开始运行,四个任务并发执行,每个任务分批内再次并发,并且控制了每一批次完成后再进行下一批,所以总运行时间约4s,符合期望值。完整源码请阅读原文或移步GitHub:https://github.com/astraw99/edm4. 小结:本文通过两层嵌套Go 并发,模拟实现了高性能并发EDM,具体的一些出错行控制、任务中断与再次执行将在下次继续讨论,主要逻辑已跑通,几个坑点小结如下:a) WaitGroup 一般用于main 主协程等待全部子协程退出后,再优雅退出主协程;嵌套使用时注意wg.Wait()放的位置;b) 合理使用channel,无缓冲chan将阻塞当前goroutine,有缓冲chan在cap未满的情况下不会阻塞当前goroutine,使用完记得释放chan资源;c) 注意函数间传值或传引用(本质上还是传值,传的指针的指针内存值)的合理使用;后记:第一篇博客写到这里差不多算完成了,一不小心一个下午就过去了,写的逻辑、可读性可能不太好请见谅,欢迎留言批评指正。感谢您的阅读。 ...

January 27, 2019 · 4 min · jiezi

总结了才知道,原来channel有这么多用法!

这篇文章总结了channel的10种常用操作,以一个更高的视角看待channel,会给大家带来对channel更全面的认识。在介绍10种操作前,先简要介绍下channel的使用场景、基本操作和注意事项。channel的使用场景把channel用在数据流动的地方:消息传递、消息过滤信号广播事件订阅与广播请求、响应转发任务分发结果汇总并发控制同步与异步…channel的基本操作和注意事项channel存在3种状态:nil,未初始化的状态,只进行了声明,或者手动赋值为nilactive,正常的channel,可读或者可写closed,已关闭,千万不要误认为关闭channel后,channel的值是nilchannel可进行3种操作:读写关闭把这3种操作和3种channel状态可以组合出9种情况:对于nil通道的情况,也并非完全遵循上表,有1个特殊场景:当nil的通道在select的某个case中时,这个case会阻塞,但不会造成死锁。参考代码请看:https://dave.cheney.net/2014/…下面介绍使用channel的10种常用操作。1. 使用for range读channel场景:当需要不断从channel读取数据时原理:使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。用法:for x := range ch{ fmt.Println(x)}2. 使用_,ok判断channel是否关闭场景:读channel,但不确定channel是否关闭时原理:读已关闭的channel会造成panic,如果不确定channel,需要使用ok进行检测。ok的结果和含义:true:读到数据,并且通道没有关闭。false:通道关闭,无数据读到。用法:if v, ok := <- ch; ok { fmt.Println(v)}3. 使用select处理多个channel场景:需要对多个通道进行同时处理,但只处理最先发生的channel时原理:select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的。用法:// 分配job时,如果收到关闭的通知则退出,不分配jobfunc (h *Handler) handle(job *Job) { select { case h.jobCh<-job: return case <-h.stopCh: return }}4. 使用channel的声明控制读写权限场景:协程对某个通道只读或只写时目的:A. 使代码更易读、更易维护,B. 防止只读协程对通道进行写数据,但通道已关闭,造成panic。用法:如果协程对某个channel只有写操作,则这个channel声明为只写。如果协程对某个channel只有读操作,则这个channe声明为只读。// 只有generator进行对outCh进行写操作,返回声明// <-chan int,可以防止其他协程乱用此通道,造成隐藏bugfunc generator(int n) <-chan int { outCh := make(chan int) go func(){ for i:=0;i<n;i++{ outCh<-i } }() return outCh}// consumer只读inCh的数据,声明为<-chan int// 可以防止它向inCh写数据func consumer(inCh <-chan int) { for x := range inCh { fmt.Println(x) }}5. 使用缓冲channel增强并发和异步场景:异步和并发原理:A. 有缓冲通道是异步的,无缓冲通道是同步的,B. 有缓冲通道可供多个协程同时处理,在一定程度可提高并发性。用法:// 无缓冲,同步ch1 := make(chan int)ch2 := make(chan int, 0)// 有缓冲,异步ch3 := make(chan int, 1)// 使用5个do协程同时处理输入数据func test() { inCh := generator(100) outCh := make(chan int, 10) for i := 0; i < 5; i++ { go do(inCh, outCh) } for r := range outCh { fmt.Println(r) }}func do(inCh <-chan int, outCh chan<- int) { for v := range inCh { outCh <- v * v }}6. 为操作加上超时场景:需要超时控制的操作原理:使用select和time.After,看操作和定时器哪个先返回,处理先完成的,就达到了超时控制的效果用法:func doWithTimeOut(timeout time.Duration) (int, error) { select { case ret := <-do(): return ret, nil case <-time.After(timeout): return 0, errors.New(“timeout”) }}func do() <-chan int { outCh := make(chan int) go func() { // do work }() return outCh}7. 使用time实现channel无阻塞读写场景:并不希望在channel的读写上浪费时间原理:是为操作加上超时的扩展,这里的操作是channel的读或写用法:func unBlockRead(ch chan int) (x int, err error) { select { case x = <-ch: return x, nil case <-time.After(time.Microsecond): return 0, errors.New(“read time out”) }}func unBlockWrite(ch chan int, x int) (err error) { select { case ch <- x: return nil case <-time.After(time.Microsecond): return errors.New(“read time out”) }}注:time.After等待可以替换为default,则是channel阻塞时,立即返回的效果8. 使用close(ch)关闭所有下游协程场景:退出时,显示通知所有协程退出原理:所有读ch的协程都会收到close(ch)的信号用法:func (h *Handler) Stop() { close(h.stopCh) // 可以使用WaitGroup等待所有协程退出}// 收到停止后,不再处理请求func (h *Handler) loop() error { for { select { case req := <-h.reqCh: go handle(req) case <-h.stopCh: return } }}9. 使用chan struct{}作为信号channel场景:使用channel传递信号,而不是传递数据时原理:没数据需要传递时,传递空struct用法:// 上例中的Handler.stopCh就是一个例子,stopCh并不需要传递任何数据// 只是要给所有协程发送退出的信号type Handler struct { stopCh chan struct{} reqCh chan *Request}10. 使用channel传递结构体的指针而非结构体场景:使用channel传递结构体数据时原理:channel本质上传递的是数据的拷贝,拷贝的数据越小传输效率越高,传递结构体指针,比传递结构体更高效用法:reqCh chan *Request// 好过reqCh chan Request你有哪些channel的奇淫巧技,说来看看?如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/01/20/golang-channel-all-usage/ ...

January 21, 2019 · 2 min · jiezi

来,控制一下 Goroutine 的并发数量

原文地址:来,控制一下 Goroutine 的并发数量问题func main() { userCount := math.MaxInt64 for i := 0; i < userCount; i++ { go func(i int) { // 做一些各种各样的业务逻辑处理 fmt.Printf(“go func: %d\n”, i) time.Sleep(time.Second) }(i) }}在这里,假设 userCount 是一个外部传入的参数(不可预测,有可能值非常大),有人会全部丢进去循环。想着全部都并发 goroutine 去同时做某一件事。觉得这样子会效率会更高,对不对!那么,你觉得这里有没有什么问题?噩梦般的开始当然,在特定场景下,问题可大了。因为在本文被丢进去同时并发的可是一个极端值。我们可以一起观察下图的指标分析,看看情况有多 “崩溃”。下图是上述代码的表现:输出结果…go func: 5839go func: 5840go func: 5841go func: 5842go func: 5915go func: 5524go func: 5916go func: 8209go func: 8264signal: killed如果你自己执行过代码,在 “输出结果” 上你会遇到如下问题:系统资源占用率不断上涨输出一定数量后:控制台就不再刷新输出最新的值了信号量:signal: killed系统负载CPU短时间内系统负载暴增虚拟内存短时间内占用的虚拟内存暴增topPID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS…73414 test 100.2 01:59.50 9/1 0 18 6801M+ 0B 114G+ 73403 73403 running *0[1]小结如果仔细看过监控工具的示意图,就可以知道其实我间隔的执行了两次,能看到系统间的使用率幅度非常大。当进程被杀掉后,整体又恢复为正常值在这里,我们回到主题,就是在不控制并发的 goroutine 数量 会发生什么问题?大致如下:CPU 使用率浮动上涨Memory 占用不断上涨。也可以看看 CMPRS,它表示进程的压缩数据的字节数。已经到达 114G+ 了主进程崩溃(被杀掉了)简单来说,“崩溃” 的原因就是对系统资源的占用过大。常见的比如:打开文件数(too many files open)、内存占用等等危害对该台服务器产生非常大的影响,影响自身及相关联的应用。很有可能导致不可用或响应缓慢,另外启动了复数 “失控” 的 goroutine,导致程序流转混乱解决方案在前面花了大量篇幅,渲染了在存在大量并发 goroutine 数量时,不控制的话会出现 “严重” 的问题,接下来一起思考下解决方案。如下:控制/限制 goroutine 同时并发运行的数量改变应用程序的逻辑写法(避免大规模的使用系统资源和等待)调整服务的硬件配置、最大打开数、内存等阈值控制 goroutine 并发数量接下来正式的开始解决这个问题,希望你认真阅读的同时加以思考,因为这个问题在实际项目中真的是太常见了!问题已经抛出来了,你需要做的是想想有什么办法解决这个问题。建议你自行思考一下技术方案。再接着往下看 :-)尝试 chanfunc main() { userCount := 10 ch := make(chan bool, 2) for i := 0; i < userCount; i++ { ch <- true go Read(ch, i) } //time.Sleep(time.Second)}func Read(ch chan bool, i int) { fmt.Printf(“go func: %d\n”, i) <- ch}输出结果:go func: 1go func: 2go func: 3go func: 4go func: 5go func: 6go func: 7go func: 8go func: 0嗯,我们似乎很好的控制了 2 个 2 个的 “顺序” 执行多个 goroutine。但是,问题出现了。你仔细数一下输出结果,才 9 个值?这明显就不对。原因出在当主协程结束时,子协程也是会被终止掉的。因此剩余的 goroutine 没来及把值输出,就被送上路了(不信你把 time.Sleep 打开看看,看看输出数量)尝试 sync…var wg = sync.WaitGroup{}func main() { userCount := 10 for i := 0; i < userCount; i++ { wg.Add(1) go Read(i) } wg.Wait()}func Read(i int) { defer wg.Done() fmt.Printf(“go func: %d\n”, i)}嗯,单纯的使用 sync.WaitGroup 也不行。没有控制到同时并发的 goroutine 数量(代指达不到本文所要求的目标)小结单纯简单使用 channel 或 sync 都有明显缺陷,不行。我们再看看组件配合能不能实现尝试 chan + sync…var wg = sync.WaitGroup{}func main() { userCount := 10 ch := make(chan bool, 2) for i := 0; i < userCount; i++ { go Read(ch, i) } wg.Wait()}func Read(ch chan bool, i int) { defer wg.Done() wg.Add(1) ch <- true fmt.Printf(“go func: %d, time: %d\n”, i, time.Now().Unix()) time.Sleep(time.Second) <-ch}输出结果:go func: 9, time: 1547911938go func: 1, time: 1547911938go func: 6, time: 1547911939go func: 7, time: 1547911939go func: 8, time: 1547911940go func: 0, time: 1547911940go func: 3, time: 1547911941go func: 2, time: 1547911941go func: 4, time: 1547911942go func: 5, time: 1547911942从输出结果来看,确实实现了控制 goroutine 以 2 个 2 个的数量去执行我们的 “业务逻辑”,当然结果集也理所应当的是乱序输出方案一:简单 Semaphore在确立了简单使用 chan + sync 的方案是可行后,我们重新将流转逻辑封装为 gsema,主程序变成如下:import ( “fmt” “time” “github.com/EDDYCJY/gsema”)var sema = gsema.NewSemaphore(3)func main() { userCount := 10 for i := 0; i < userCount; i++ { go Read(i) } sema.Wait()}func Read(i int) { defer sema.Done() sema.Add(1) fmt.Printf(“go func: %d, time: %d\n”, i, time.Now().Unix()) time.Sleep(time.Second)}分析方案在上述代码中,程序执行流程如下:设置允许的并发数目为 3 个循环 10 次,每次启动一个 goroutine 来执行任务每一个 goroutine 在内部利用 sema 进行调控是否阻塞按允许并发数逐渐释出 goroutine,最后结束任务看上去人模人样,没什么严重问题。但却有一个 “大” 坑,认真看到第二点 “每次启动一个 goroutine” 这句话。这里有点问题,提前产生那么多的 goroutine 会不会有什么问题,接下来一起分析下利弊,如下:利适合量不大、复杂度低的使用场景几百几千个、几十万个也是可以接受的(看具体业务场景)实际业务逻辑在运行前就已经被阻塞等待了(因为并发数受限),基本实际业务逻辑损耗的性能比 goroutine 本身大goroutine 本身很轻便,仅损耗极少许的内存空间和调度。这种等待响应的情况都是躺好了,等待任务唤醒Semaphore 操作复杂度低且流转简单,容易控制弊不适合量很大、复杂度高的使用场景有几百万、几千万个 goroutine 的话,就浪费了大量调度 goroutine 和内存空间。恰好你的服务器也接受不了的话Semaphore 操作复杂度提高,要管理更多的状态小结基于什么业务场景,就用什么方案去做事有足够的时间,允许你去追求更优秀、极致的方案(用第三方库也行)用哪种方案,我认为主要基于以上两点去思考,都是 OK 的。没有对错,只有当前业务场景能不能接受,这个预先启动的 goroutine 数量你的系统是否能够接受当然了,常见/简单的 Go 应用采用这类技术方案,基本就能解决问题了。因为像本文第一节 “问题” 如此超巨大数量的情况,情况很少。其并不存在那些 “特殊性”。因此用这个方案基本 OK灵活控制 goroutine 并发数量小手一紧。隔壁老王发现了新的问题。“方案一” 中,在输入输出一体的情况下,在常见的业务场景中确实可以但,这次新的业务场景比较特殊,要控制输入的数量,以此达到改变允许并发运行 goroutine 的数量。我们仔细想想,要做出如下改变:输入/输出要抽离,才可以分别控制输入/输出要可变,理所应当在 for-loop 中(可设置数值的地方)允许改变 goroutine 并发数量,但它也必须有一个最大值(因为允许改变是相对)方案二:灵活 chan + syncpackage mainimport ( “fmt” “sync” “time”)var wg sync.WaitGroupfunc main() { userCount := 10 ch := make(chan int, 5) for i := 0; i < userCount; i++ { wg.Add(1) go func() { defer wg.Done() for d := range ch { fmt.Printf(“go func: %d, time: %d\n”, d, time.Now().Unix()) time.Sleep(time.Second * time.Duration(d)) } }() } for i := 0; i < 10; i++ { ch <- 1 ch <- 2 //time.Sleep(time.Second) } close(ch) wg.Wait()}输出结果:…go func: 1, time: 1547950567go func: 3, time: 1547950567go func: 1, time: 1547950567go func: 2, time: 1547950567go func: 2, time: 1547950567go func: 3, time: 1547950567go func: 1, time: 1547950568go func: 2, time: 1547950568go func: 3, time: 1547950568go func: 1, time: 1547950568go func: 3, time: 1547950569go func: 2, time: 1547950569在 “方案二” 中,我们可以随时随地的根据新的业务需求,做如下事情:变更 channel 的输入数量能够根据特殊情况,变更 channel 的循环值变更最大允许并发的 goroutine 数量总的来说,就是可控空间都尽量放开了,是不是更加灵活了呢 :-)方案三:第三方库go-playground/poolnozzle/throttlerJeffail/tunny比较成熟的第三方库也不少,基本都是以生成和管理 goroutine 为目标的池工具。我简单列了几个,具体建议大家阅读下源码或者多找找,原理相似总结在本文的开头,我花了大力气(极端数量),告诉你同时并发过多的 goroutine 数量会导致系统占用资源不断上涨。最终该服务崩盘的极端情况。为的是希望你今后避免这种问题,给你留下深刻的印象接下来我们以 “控制 goroutine 并发数量” 为主题,展开了一番分析。分别给出了三种方案。在我看来,各具优缺点,我建议你挑选合适自身场景的技术方案就可以了因为,有不同类型的技术方案也能解决这个问题,千人千面。本文推荐的是较常见的解决方案,也欢迎大家在评论区继续补充 :-) ...

January 20, 2019 · 3 min · jiezi

深入Redis持久化

一、Redis高可用概述在介绍Redis高可用之前,先说明一下在Redis的语境中高可用的含义。我们知道,在web服务器中,高可用是指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常服务(99.9%、99.99%、99.999% 等等)。但是在Redis语境中,高可用的含义似乎要宽泛一些,除了保证提供正常服务(如主从分离、快速容灾技术),还需要考虑数据容量的扩展、数据安全不会丢失等。在Redis中,实现高可用的技术主要包括持久化、复制、哨兵和集群,下面分别说明它们的作用,以及解决了什么样的问题。持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。复制:复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制。哨兵:在复制的基础上,哨兵实现了自动化的故障恢复。缺陷:写操作无法负载均衡;存储能力受到单机的限制。集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。二、Redis持久化概述持久化的功能:Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘;当下次Redis重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。Redis持久化分为RDB持久化和AOF持久化:前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog);由于AOF持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式,不过RDB持久化仍然有其用武之地。下面依次介绍RDB持久化和AOF持久化;由于Redis各个版本之间存在差异,如无特殊说明,以Redis3.0为准。三、RDB持久化RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。触发条件RDB持久化的触发分为手动触发和自动触发两种。1) 手动触发save命令和bgsave命令都可以生成RDB文件。save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求。而bgsave命令会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求。此时服务器执行日志如下:bgsave命令执行过程中,只有fork子进程时会阻塞服务器,而对于save命令,整个过程都会阻塞服务器,因此save已基本被废弃,线上环境要杜绝save的使用;后文中也将只介绍bgsave命令。此外,在自动触发RDB持久化时,Redis也会选择bgsave而不是save来进行持久化;下面介绍自动触发RDB持久化的条件。2) 自动触发save m n自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。例如,查看redis的默认配置文件(Linux下为redis根目录下的redis.conf),可以看到如下配置信息:其中save 900 1的含义是:当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave;save 300 10和save 60 10000同理。当三个save条件满足任意一个时,都会引起bgsave的调用。save m n的实现原理Redis的save m n,是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查 save m n 配置的条件是否满足,如果满足就执行bgsave。dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。例如,如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令。lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:(1)当前时间-lastsave > m(2)dirty >= nsave m n 执行日志下图是save m n触发bgsave执行时,服务器打印日志的情况:其他自动触发机制除了save m n 以外,还有一些其他情况会触发bgsave:在主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点执行shutdown命令时,自动执行rdb持久化,如下图所示:执行流程前面介绍了触发bgsave的条件,下面将说明bgsave命令的执行流程,如下图所示(图片来源:https://blog.csdn.net/a100772…:图片中的5个步骤所进行的操作如下:1) Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。2) 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令3) 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令4) 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换5) 子进程发送信号给父进程表示完成,父进程更新统计信息RDB文件RDB文件是经过压缩的二进制文件,下面介绍关于RDB文件的一些细节。存储路径RDB文件的存储路径既可以在启动前配置,也可以通过命令动态设定。配置:dir配置指定目录,dbfilename指定文件名。默认是Redis根目录下的dump.rdb文件。动态设定:Redis启动后也可以动态修改RDB存储路径,在磁盘损害或空间不足时非常有用;执行命令为config set dir {newdir}和config set dbfilename {newFileName}。如下所示(Windows环境):RDB文件格式RDB文件格式如下图所示(图片来源:《Redis设计与实现》):其中各个字段的含义说明如下:1) REDIS:常量,保存着”REDIS”5个字符。2) db_version:RDB文件的版本号,注意不是Redis的版本号。3) SELECTDB 0 pairs:表示一个完整的数据库(0号数据库),同理SELECTDB 3 pairs表示完整的3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。4) EOF:常量,标志RDB文件正文内容结束。5) check_sum:前面所有内容的校验和;Redis在载入RBD文件时,会计算前面的校验和并与check_sum值比较,判断文件是否损坏。压缩Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭:需要注意的是,RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。启动时加载RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。Redis启动日志中可以看到自动载入的执行:Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。RDB常用配置总结下面是RDB常用的配置项,以及默认值;前面介绍过的这里不再详细介绍。save m n:bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发stop-writes-on-bgsave-error yes:当bgsave出现错误时,Redis是否停止执行写命令;设置为yes,则当硬盘出现问题时,可以及时发现,避免数据的大量丢失;设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为nordbcompression yes:是否开启RDB文件压缩rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用;关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现dbfilename dump.rdb:RDB文件名dir ./:RDB文件和AOF文件所在目录四、AOF持久化RDB持久化是将进程数据写入文件,而AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案。开启AOFRedis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:appendonly yes执行流程由于需要记录Redis的每条写命令,因此AOF不需要触发,下面介绍AOF的执行流程。AOF的执行流程包括:命令追加(append):将Redis的写命令追加到缓冲区aof_buf;文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;文件重写(rewrite):定期重写AOF文件,达到压缩的目的。1) 命令追加(append)Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点;具体格式略。在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。2) 文件写入(write)和文件同步(sync)Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数,说明如下:为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。3) 文件重写(rewrite)随着时间流逝,Redis服务器执行的写命令越来越多,AOF文件也会越来越大;过大的AOF文件不仅会影响服务器的正常运行,也会导致数据恢复需要的时间过长。文件重写是指定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作!关于文件重写需要注意的另一点是:对于AOF持久化来说,文件重写虽然是强烈推荐的,但并不是必须的;即使没有文件重写,数据也可以被持久化并在Redis启动的时候导入;因此在一些实现中,会关闭自动的文件重写,然后通过定时任务在每天的某一时刻定时执行。文件重写之所以能够压缩AOF文件,原因在于:过期的数据不再写入文件无效的命令不再写入文件:如有些数据被重复设值(set mykey v1, set mykey v2)、有些数据被删除了(sadd myset v1, del myset)等等多条命令可以合并为一个:如sadd myset v1, sadd myset v2, sadd myset v3可以合并为sadd myset v1 v2 v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义,不可更改,3.0版本中值是64。通过上述内容可以看出,由于重写后AOF执行的命令减少了,文件重写既可以减少文件占用的空间,也可以加快恢复速度。文件重写的触发文件重写的触发,分为手动触发和自动触发:手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。此时服务器执行日志如下:自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。auto-aof-rewrite-min-size:执行AOF重写时,文件的最小体积,默认值为64MB。auto-aof-rewrite-percentage:执行AOF重写时,当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值。其中,参数可以通过config get命令查看:状态可以通过info persistence查看:只有当auto-aof-rewrite-min-size和auto-aof-rewrite-percentage两个参数同时满足时,才会自动触发AOF重写,即bgrewriteaof操作。自动触发bgrewriteaof时,可以看到服务器日志如下:文件重写的流程文件重写流程如下图所示(图片来源:http://www.cnblogs.com/yangmi…:关于文件重写的流程,有两点需要特别注意:(1)重写由父进程fork子进程进行;(2)重写期间Redis执行的写命令,需要追加到新的AOF文件中,为此Redis引入了aof_rewrite_buf缓存。对照上图,文件重写的流程如下:1) Redis父进程首先判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。前面曾介绍过,这个主要是基于性能方面的考虑。2) 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。3.1) 父进程fork后,bgrewriteaof命令返回”Background append only file rewrite started”信息并不再阻塞父进程,并可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。3.2) 由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。4) 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。5.1) 子进程写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过info persistence查看。5.2) 父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。5.3) 使用新的AOF文件替换老文件,完成AOF重写。启动时加载前面提到过,当AOF开启时,Redis启动时会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会载入RDB文件恢复数据。当AOF开启,且AOF文件存在时,Redis启动日志:当AOF开启,但AOF文件不存在时,即使RDB文件存在也不会加载(更早的一些版本可能会加载,但3.0不会),Redis启动日志如下:文件校验与载入RDB文件类似,Redis载入AOF文件时,会对AOF文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。但如果是AOF文件结尾不完整(机器突然宕机等容易导致文件尾部不完整),且aof-load-truncated参数开启,则日志中会输出警告,Redis忽略掉AOF文件的尾部,启动成功。aof-load-truncated参数默认是开启的:伪客户端因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时命令是直接从文件中读取的,并不是由客户端发送;因此Redis服务器在载入AOF文件之前,会创建一个没有网络连接的客户端,之后用它来执行AOF文件中的命令,命令执行的效果与带网络连接的客户端完全一样。AOF常用配置总结下面是AOF常用的配置项,以及默认值;前面介绍过的这里不再详细介绍。appendonly no:是否开启AOFappendfilename “appendonly.aof”:AOF文件名dir ./:RDB文件和AOF文件所在目录appendfsync everysec:fsync持久化策略no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡auto-aof-rewrite-percentage 100:文件重写触发条件之一auto-aof-rewrite-min-size 64mb:文件重写触发提交之一aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件五、方案选择与常见问题前面介绍了RDB和AOF两种持久化方案的细节,下面介绍RDB和AOF的特点、如何选择持久化方案,以及在持久化过程中常遇到的问题等。RDB和AOF的优缺点RDB和AOF各有优缺点:RDB持久化优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。AOF持久化与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。持久化策略选择在介绍持久化策略之前,首先要明白无论是RDB还是AOF,持久化的开启都是要付出性能方面代价的:对于RDB持久化,一方面是bgsave在进行fork操作时Redis主进程会阻塞,另一方面,子进程向硬盘写数据也会带来IO压力;对于AOF持久化,向硬盘写数据的频率大大提高(everysec策略下为秒级),IO压力更大,甚至可能造成AOF追加阻塞问题(后面会详细介绍这种阻塞),此外,AOF文件的重写与RDB的bgsave类似,会有fork时的阻塞和子进程的IO压力问题。相对来说,由于AOF向硬盘中写数据的频率更高,因此对Redis主进程性能的影响会更大。在实际生产环境中,根据数据量、应用对数据的安全要求、预算限制等不同情况,会有各种各样的持久化策略;如完全不使用任何持久化、使用RDB或AOF的一种,或同时开启RDB和AOF持久化等。此外,持久化的选择必须与Redis的主从策略一起考虑,因为主从复制与持久化同样具有数据备份的功能,而且主机master和从机slave可以独立的选择持久化方案。下面分场景来讨论持久化策略的选择,下面的讨论也只是作为参考,实际方案可能更复杂更具多样性。(1)如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化。(2)在单机环境下(对于个人开发者,这种情况可能比较常见),如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF。(3)但在多数情况下,我们都会配置主从环境,slave的存在既可以实现数据的热备,也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。在这种情况下,一种可行的做法是:master:完全关闭持久化(包括RDB和AOF),这样可以让master的性能达到最好slave:关闭RDB,开启AOF(如果对数据安全要求不高,开启RDB关闭AOF也可以),并定时对持久化文件进行备份(如备份到其他文件夹,并标记好备份的时间);然后关闭AOF的自动重写,然后添加定时任务,在每天Redis闲时(如凌晨12点)调用bgrewriteaof。这里需要解释一下,为什么开启了主从复制,可以实现数据的热备份,还需要设置持久化呢?因为在一些特殊情况下,主从复制仍然不足以保证数据的安全,例如:master和slave进程同时停止:考虑这样一种场景,如果master和slave在同一栋大楼或同一个机房,则一次停电事故就可能导致master和slave机器同时关机,Redis进程停止;如果没有持久化,则面临的是数据的完全丢失。master误重启:考虑这样一种场景,master服务因为故障宕掉了,如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的;如果master和slave都没有持久化,同样会面临数据的完全丢失。需要注意的是,即便是使用了哨兵(关于哨兵后面会有文章介绍)进行自动的主从切换,也有可能在哨兵轮询到master之前,便被自动拉起机制重启了。因此,应尽量避免“自动拉起机制”和“不做持久化”同时出现。(4)异地灾备:上述讨论的几种持久化策略,针对的都是一般的系统故障,如进程异常退出、宕机、断电等,这些故障不会损坏硬盘。但是对于一些可能导致硬盘损坏的灾难情况,如火灾地震,就需要进行异地灾备。例如对于单机的情形,可以定时将RDB文件或重写后的AOF文件,通过scp拷贝到远程机器,如阿里云、AWS等;对于主从的情形,可以定时在master上执行bgsave,然后将RDB文件拷贝到远程机器,或者在slave上执行bgrewriteaof重写AOF文件后,将AOF文件拷贝到远程机器上。一般来说,由于RDB文件文件小、恢复快,因此灾难恢复常用RDB文件;异地备份的频率根据数据安全性的需要及其他条件来确定,但最好不要低于一天一次。fork阻塞:CPU的阻塞在Redis的实践中,众多因素限制了Redis单机的内存不能过大,例如:当面对请求的暴增,需要从库扩容时,Redis内存过大会导致扩容时间太长;当主机宕机时,切换主机后需要挂载从库,Redis内存过大导致挂载速度过慢;以及持久化过程中的fork操作,下面详细说明。首先说明一下fork操作:父进程通过fork操作可以创建子进程;子进程创建后,父子进程共享代码段,不共享进程的数据空间,但是子进程会获得父进程的数据空间的副本。在操作系统fork的实际实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间;但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。虽然fork时,子进程不会复制父进程的数据空间,但是会复制内存页表(页表相当于内存的索引、目录);父进程的数据空间越大,内存页表越大,fork时复制耗时也会越多。在Redis中,无论是RDB持久化的bgsave,还是AOF重写的bgrewriteaof,都需要fork出子进程来进行操作。如果Redis内存过大,会导致fork操作时复制内存页表耗时过多;而Redis主进程在进行fork时,是完全阻塞的,也就意味着无法响应客户端的请求,会造成请求延迟过大。对于不同的硬件、不同的操作系统,fork操作的耗时会有所差别,一般来说,如果Redis单机内存达到了10GB,fork时耗时可能会达到百毫秒级别(如果使用Xen虚拟机,这个耗时可能达到秒级别)。因此,一般来说Redis单机内存一般要限制在10GB以内;不过这个数据并不是绝对的,可以通过观察线上环境fork的耗时来进行调整。观察的方法如下:执行命令info stats,查看latest_fork_usec的值,单位为微秒。为了减轻fork操作带来的阻塞问题,除了控制Redis单机内存的大小以外,还可以适度放宽AOF重写的触发条件、选用物理机或高效支持fork操作的虚拟化技术等,例如使用Vmware或KVM虚拟机,不要使用Xen虚拟机。AOF追加阻塞:硬盘的阻塞前面提到过,在AOF中,如果AOF缓冲区的文件同步策略为everysec,则:在主线程中,命令写入aof_buf后调用系统write操作,write完成后主线程返回;fsync同步文件操作由专门的文件同步线程每秒调用一次。这种做法的问题在于,如果硬盘负载过高,那么fsync操作可能会超过1s;如果Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快;如果此时Redis进程异常退出,丢失的数据也会越来越多,可能远超过1s。为此,Redis的处理策略是这样的:主线程每次进行AOF会对比上次fsync成功的时间;如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;此外,使用everysec配置,AOF最多可能丢失2s的数据,而不是1s。AOF追加阻塞问题定位的方法:(1)监控info Persistence中的aof_delayed_fsync:当AOF追加阻塞发生时(即主线程等待fsync而阻塞),该指标累加。(2)AOF阻塞时的Redis日志:Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.(3)如果AOF追加阻塞频繁发生,说明系统的硬盘负载太大;可以考虑更换IO速度更快的硬盘,或者通过IO监控分析工具对系统的IO负载进行分析,如iostat(系统级io)、iotop(io版的top)、pidstat等。info命令与持久化前面提到了一些通过info命令查看持久化相关状态的方法,下面来总结一下。(1)info Persistence执行结果如下:其中比较重要的包括:rdb_last_bgsave_status:上次bgsave 执行结果,可以用于发现bgsave错误rdb_last_bgsave_time_sec:上次bgsave执行时间(单位是s),可以用于发现bgsave是否耗时过长aof_enabled:AOF是否开启aof_last_rewrite_time_sec: 上次文件重写执行时间(单位是s),可以用于发现文件重写是否耗时过长aof_last_bgrewrite_status: 上次bgrewrite执行结果,可以用于发现bgrewrite错误aof_buffer_length和aof_rewrite_buffer_length:aof缓存区大小和aof重写缓冲区大小aof_delayed_fsync:AOF追加阻塞情况的统计(2)info stats其中与持久化关系较大的是:latest_fork_usec,代表上次fork耗时,可以参见前面的讨论。六、总结本文主要内容可以总结如下:1、持久化在Redis高可用中的作用:数据备份,与主从复制相比强调的是由内存到硬盘的备份。2、RDB持久化:将数据快照备份到硬盘;介绍了其触发条件(包括手动出发和自动触发)、执行流程、RDB文件等,特别需要注意的是文件保存操作由fork出的子进程来进行。3、AOF持久化:将执行的写命令备份到硬盘(类似于MySQL的binlog),介绍了其开启方法、执行流程等,特别需要注意的是文件同步策略的选择(everysec)、文件重写的流程。4、一些现实的问题:包括如何选择持久化策略,以及需要注意的fork阻塞、AOF追加阻塞等。 ...

January 16, 2019 · 1 min · jiezi

Go并发调用的超时处理

之前有聊过 golang 的协程,我发觉似乎还很理论,特别是在并发安全上,所以特结合网上的一些例子,来试验下go routine中 的 channel, select, context 的妙用。场景-微服务调用我们用 gin(一个web框架) 作为处理请求的工具,没有安装过的话,需求是这样的:一个请求 X 会去并行调用 A, B, C 三个方法,并把三个方法返回的结果加起来作为 X 请求的 Response。但是我们这个 Response 是有时间要求的(不能超过3秒的响应时间),可能 A, B, C 中任意一个或两个,处理逻辑十分复杂,或者数据量超大,导致处理时间超出预期,那么我们就马上切断,并返回已经拿到的任意个返回结果之和。我们先来定义主函数:func main() { r := gin.New() r.GET("/calculate", calHandler) http.ListenAndServe(":8008", r)}非常简单,普通的请求接受和 handler 定义。其中 calHandler 是我们用来处理请求的函数。分别定义三个假的微服务,其中第三个将会是我们超时的哪位func microService1() int { time.Sleep(1time.Second) return 1}func microService2() int { time.Sleep(2time.Second) return 2}func microService3() int { time.Sleep(10*time.Second) return 3}接下来,我们看看 calHandler 里到底是什么func calHandler(c *gin.Context) { …}要点1–并发调用直接用 go 就好了嘛所以一开始我们可能就这么写:go microService1()go microService2()go microService3()很简单有没有,但是等等,说好的返回值我怎么接呢?为了能够并行地接受处理结果,我们很容易想到用 channel 去接。所以我们把调用服务改成这样:var resChan = make(chan int, 3) // 因为有3个结果,所以我们创建一个可以容纳3个值的 int channel。go func() { resChan <- microService1()}()go func() { resChan <- microService2()}()go func() { resChan <- microService3()}()有东西接,那也要有方法去算,所以我们加一个一直循环拿 resChan 中结果并计算的方法:var resContainer, sum intfor { resContainer = <-resChan sum += resContainer}这样一来我们就有一个 sum 来计算每次从 resChan 中拿出的结果了。要点2–超时信号还没结束,说好的超时处理呢?为了实现超时处理,我们需要引入一个东西,就是 context,什么是 context ?我们这里只使用 context 的一个特性,超时通知(其实这个特性完全可以用 channel 来替代)。可以看在定义 calHandler 的时候我们已经将 c gin.Context 作为参数传了进来,那我们就不用自己在声明了。gin.Context 简单理解为贯穿整个 gin 声明周期的上下文容器,有点像是分身,亦或是量子纠缠的感觉。有了这个 gin.Context, 我们就能在一个地方对 context 做出操作,而其他正在使用 context 的函数或方法,也会感受到 context 做出的变化。ctx, _ := context.WithTimeout(c, 3time.Second) //定义一个超时的 context只要时间到了,我们就能用 ctx.Done() 获取到一个超时的 channel(通知),然后其他用到这个 ctx 的地方也会停掉,并释放 ctx。一般来说,ctx.Done() 是结合 select 使用的。所以我们又需要一个循环来监听 ctx.Done()for { select { case <- ctx.Done(): // 返回结果}现在我们有两个 for 了,是不是能够合并下?for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- ctx.Done(): fmt.Println(“result:”, sum) return }}诶嘿,看上去不错。不过我们怎么在正常完成微服务调用的时候输出结果呢?看来我们还需要一个 flagvar count intfor { select { case resContainer = <-resChan: sum += resContainer count ++ fmt.Println(“add”, resContainer) if count > 2 { fmt.Println(“result:”, sum) return } case <- ctx.Done(): fmt.Println(“timeout result:”, sum) return }}我们加入一个计数器,因为我们只是调用3次微服务,所以当 count 大于2的时候,我们就应该结束并输出结果了。要点3–并发中的等待这是一种偷懒的方法,因为我们知道了调用微服务的次数,如果我们并不知道,或者之后还要添加呢?手动每次改 count 的判断阈值会不会太沙雕了?这时候我们就要加入 sync 包了。我们将会使用的 sync 的一个特性是 WaitGroup。它的作用是等待一组协程运行完毕后,执行接下去的步骤。我们来改下之前微服务调用的代码块:var success = make(chan int, 1) // 成功的通道标识wg := sync.WaitGroup{} // 创建一个 waitGroup 组wg.Add(3) // 我们往组里加3个标识,因为我们要运行3个任务go func() { resChan <- microService1() wg.Done() // 完成一个,Done()一个}()go func() { resChan <- microService2() wg.Done()}()go func() { resChan <- microService3() wg.Done()}()wg.Wait() // 直到我们前面三个标识都被 Done 了,否则程序一直会阻塞在这里success <- 1 // 我们发送一个成功信号到通道中既然我们有了 success 这个信号,那么再把它加入到监控 for 循环中,并做些修改,删除原来 count 判断的部分。go func() { for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- success: fmt.Println(“result:”, sum) return case <- ctx.Done(): fmt.Println(“result:”, sum) return } }}()三个 case,分工明确,一个用来拿服务输出的结果并计算,一个用来做最终的完成输出,一个是超时输出。同时我们将这个循环监听,也作为协程运行。至此,所有的主要代码都完成了。下面是完全版package mainimport ( “context” “fmt” “net/http” “sync” “time” “github.com/gin-gonic/gin”)// 一个请求会触发调用三个服务,每个服务输出一个 int,// 请求要求结果为三个服务输出 int 之和// 请求返回时间不超过3秒,大于3秒只输出已经获得的 int 之和func calHandler(c gin.Context) { var resContainer, sum int var success, resChan = make(chan int), make(chan int, 3) ctx, _ := context.WithTimeout(c, 3time.Second) go func() { for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- success: fmt.Println(“result:”, sum) return case <- ctx.Done(): fmt.Println(“result:”, sum) return } } }() wg := sync.WaitGroup{} wg.Add(3) go func() { resChan <- microService1() wg.Done() }() go func() { resChan <- microService2() wg.Done() }() go func() { resChan <- microService3() wg.Done() }() wg.Wait() success <- 1 return}func main() { r := gin.New() r.GET("/calculate", calHandler) http.ListenAndServe(":8008", r)}func microService1() int { time.Sleep(1time.Second) return 1}func microService2() int { time.Sleep(2time.Second) return 2}func microService3() int { time.Sleep(10*time.Second) return 3}上面的程序只是简单描述了一个调用其他微服务超时的处理场景。实际过程中还需要加很多很多调料,才能保证接口的对外完整性。大家,讲究看下吧~啊哈哈哈哈 ...

January 13, 2019 · 3 min · jiezi

AbstractQueuedSynchronizer超详细原理解析

今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供队列式同步器,比如说常用的ReentranLock,Semaphore和CountDownLatch等。 为了方便理解,我们以一段使用ReentranLock的代码为例,讲解ReentranLock每个方法中有关AQS的使用。ReentranLock示例 我们都知道ReentranLock的加锁行为和Synchronized类似,都是可重入的锁,但是二者的实现方式确实完全不同的,我们之后也会讲解Synchronized的原理。除此之外,Synchronized的阻塞无法被中断,而ReentrantLock则提供了可中断的阻塞。下面的代码是ReentranLock的函数,我们就以此为顺序,依次讲解这些函数背后的实现原理。ReentrantLock lock = new ReentrantLock();lock.lock();lock.unlock();公平锁和非公平锁 ReentranLock分为公平锁和非公平锁,二者的区别就在获取锁机会是否和排队顺序相关。我们都知道,如果锁被另一个线程持有,那么申请锁的其他线程会被挂起等待,加入等待队列。理论上,先调用lock函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边。如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁。而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平。public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 我们会发现FairSync和NonfairSync都继承了Sync类,而Sync的父类就是AbstractQueuedSynchronizer(后续简称AQS)。但是AQS的构造函数是空的,并没有任何操作。 之后的源码分析,如果没有特别说明,就是指公平锁。lock操作 ReentranLock的lock函数如下所示,直接调用了sync的lock函数。也就是调用了FairSync的lock函数。 //ReentranLock public void lock() { sync.lock(); } //FairSync final void lock() { //调用了AQS的acquire函数,这是关键函数之一 acquire(1); } 我们接下来就正式开始AQS相关的源码分析了,acquire函数的作用是获取同一时间段内只能被一个线程获取的量,这个量就是抽象化的锁概念。我们先分析代码,你慢慢就会明白其中的含义。public final void acquire(int arg) { // tryAcquire先尝试获取"锁",获取了就不进入后续流程 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //addWaiter是给当前线程创建一个节点,并将其加入等待队列 //acquireQueued是当线程已经加入等待队列之后继续尝试获取锁. selfInterrupt();} tryAcquire,addWaiter和acquireQueued都是十分重要的函数,我们接下来依次学习一下这些函数,理解它们的作用。//AQS类中的变量.private volatile int state;//这是FairSync的实现,AQS中未实现,子类按照自己的需要实现该函数protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取AQS中的state变量,代表抽象概念的锁. int c = getState(); if (c == 0) { //值为0,那么当前独占性变量还未被线程占有 //如果当前阻塞队列上没有先来的线程在等待,UnfairSync这里的实现就不一致 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //成功cas,那么代表当前线程获得该变量的所有权,也就是说成功获得锁 setExclusiveOwnerThread(current); // setExclusiveOwnerThread将本线程设置为独占性变量所有者线程 return true; } } else if (current == getExclusiveOwnerThread()) { //如果该线程已经获取了独占性变量的所有权,那么根据重入性 //原理,将state值进行加1,表示多次lock //由于已经获得锁,该段代码只会被一个线程同时执行,所以不需要 //进行任何并行处理 int nextc = c + acquires; if (nextc < 0) throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; } //上述情况都不符合,说明获取锁失败 return false;} 由上述代码我们可以发现,tryAcquire就是尝试获取那个线程独占的变量state。state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁。但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,就增加state的值。 这里有几点需要说明一下,首先是compareAndSetState函数,这是使用CAS操作来设置state的值,而且state值设置了volatile修饰符,通过这两点来确保修改state的值不会出现多线程问题。然后是公平锁和非公平锁的区别问题,在UnfairSync的nonfairTryAcquire函数中不会在相同的位置上调用hasQueuedPredecessors来判断当前是否已经有线程在排队等待获得锁。 如果tryAcquire返回true,那么就是获取锁成功;如果返回false,那么就是未获得锁,需要加入阻塞等待队列。我们下面就来看一下addWaiter的相关操作。等待锁的阻塞队列 将保存当前线程信息的节点加入到等待队列的相关函数中涉及到了无锁队列的相关算法,由于在AQS中只是将节点添加到队尾,使用到的无锁算法也相对简单。真正的无锁队列的算法我们等到分析ConcurrentSkippedListMap时在进行讲解。private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //先使用快速入列法来尝试一下,如果失败,则进行更加完备的入列算法. //只有在必要的情况下才会使用更加复杂耗时的算法,也就是乐观的态度 Node pred = tail; //列尾指针 if (pred != null) { node.prev = pred; //步骤1:该节点的前趋指针指向tail if (compareAndSetTail(pred, node)){ //步骤二:cas将尾指针指向该节点 pred.next = node;//步骤三:如果成果,让旧列尾节点的next指针指向该节点. return node; } } //cas失败,或在pred == null时调用enq enq(node); return node;}private Node enq(final Node node) { for (;;) { //cas无锁算法的标准for循环,不停的尝试 Node t = tail; if (t == null) { //初始化 if (compareAndSetHead(new Node())) //需要注意的是head是一个哨兵的作用,并不代表某个要获取锁的线程节点 tail = head; } else { //和addWaiter中一致,不过有了外侧的无限循环,不停的尝试,自旋锁 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }} 通过调用addWaiter函数,AQS将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来我们就来分析一下acquireQueued函数。等待队列节点的操作 由于进入阻塞状态的操作会降低执行效率,所以,AQS会尽力避免试图获取独占性变量的线程进入阻塞状态。所以,当线程加入等待队列之后,acquireQueued会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了)。如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire判断是否应该进入阻塞状态。如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt函数来进入阻塞状态。final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //一直执行,直到获取锁,返回. final Node p = node.predecessor(); //node的前驱是head,就说明,node是将要获取锁的下一个节点. if (p == head && tryAcquire(arg)) { //所以再次尝试获取独占性变量 setHead(node); //如果成果,那么就将自己设置为head p.next = null; // help GC failed = false; return interrupted; //此时,还没有进入阻塞状态,所以直接返回false,表示不需要中断调用selfInterrupt函数 } //判断是否要进入阻塞状态.如果shouldParkAfterFailedAcquire //返回true,表示需要进入阻塞 //调用parkAndCheckInterrupt;否则表示还可以再次尝试获取锁,继续进行for循环 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //调用parkAndCheckInterrupt进行阻塞,然后返回是否为中断状态 interrupted = true; } } finally { if (failed) cancelAcquire(node); }}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //前一个节点在等待独占性变量释放的通知,所以,当前节点可以阻塞 return true; if (ws > 0) { //前一个节点处于取消获取独占性变量的状态,所以,可以跳过去 //返回false do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //将上一个节点的状态设置为signal,返回false, compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}private final boolean parkAndCheckInterrupt() { LockSupport.park(this); //将AQS对象自己传入 return Thread.interrupted();}阻塞和中断 由上述分析,我们知道了AQS通过调用LockSupport的park方法来执行阻塞当前进程的操作。其实,这里的阻塞就是线程不再执行的含义,通过调用这个函数,线程进入阻塞状态,上述的lock操作也就阻塞了,等待中断或在独占性变量被释放。public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker);//设置阻塞对象,用来记录线程被谁阻塞的,用于线程监控和分析工具来定位 UNSAFE.park(false, 0L);//让当前线程不再被线程调度,就是当前线程不再执行. setBlocker(t, null);} 关于中断的相关知识,我们以后再说,就继续沿着AQS的主线,看一下释放独占性变量的相关操作吧。unlock操作 与lock操作类似,unlock操作调用了AQS的relase方法,参数和调用acquire时一样,都是1。public final boolean release(int arg) { if (tryRelease(arg)) { //释放独占性变量,起始就是将status的值减1,因为acquire时是加1 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒head的后继节点 return true; } return false;} 由上述代码可知,release就是先调用tryRelease来释放独占性变量。如果成功,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor来唤醒他们。protected final boolean tryRelease(int releases) { //由于只有一个线程可以获得独占先变量,所以,所有操作不需要考虑多线程 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //如果等于0,那么说明锁应该被释放了,否则表示当前线程有多次lock操作. free = true; setExclusiveOwnerThread(null); } setState(c); return free;} 我们可以看到tryRelease中的逻辑也体现了可重入锁的概念,只有等到state的值为0时,才代表锁真正被释放了。所以独占性变量state的值就代表锁的有无。当state=0时,表示锁未被占有,否在表示当前锁已经被占有。private void unparkSuccessor(Node node) { ….. //一般来说,需要唤醒的线程就是head的下一个节点,但是如果它获取锁的操作被取消,或在节点为null时 //就直接继续往后遍历,找到第一个未取消的后继节点. Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);} 调用了unpark方法后,进行lock操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued中的无限for循环中的操作,再次尝试获取锁。后记 有关AQS和ReentrantLock的分析就差不多结束了。不得不说,我第一次看到AQS的实现时真是震惊,以前都认为Synchronized和ReentrantLock的实现原理是一致的,都是依靠java虚拟机的功能实现的。没有想到还有AQS这样一个背后大Boss在提供帮助啊。学习了这个类的原理,我们对JUC的很多类的分析就简单了很多。此外,AQS涉及的CAS操作和无锁队列的算法也为我们学习其他无锁算法提供了基础。知识的海洋是无限的啊! ...

January 13, 2019 · 3 min · jiezi

如何使用jMeter对某个OData服务进行高并发性能测试

For project reason I have to measure the performance of OData service being accessed parallelly. And I plan to use the open source tool JMeter to generate a huge number of request in parallel and measure the average response time. Since I am a beginner for JMeter, I write down what I have learned into this blog. I will continue to explorer the advanced feature of JMeter in my daily work.我们公司某团队开发了一个OData服务,现在我接到任务,要测试这个服务在高并发访问场景下的性能指标,比如5万个请求同时到来后,每个请求的平均响应时间,因此我选择了jMeter这个好用的工具来模拟高并发请求。Download JMeter from its official website:http://jmeter.apache.org/Go to the installation folder, add the following text in file binuser.properties:httpclient4.retrycount=1hc.parameters.file=hc.parametersCreate a new test plan for example Customer_Query_OData_test, and right click on it and create a thread group from context menu.创建一个新的测试plan,基于其再创建一个线程组:Below configuration means I would like to generate three request in parallel via three threads, each thread is executed only once. And there is no delay during the spawn of each threads ( Ramp-Up Period = 0 )下列设置意思是我想创建三个并发请求,每个请求通过一个线程实现,每个线程仅仅执行一次。每个线程派生后的延时是0秒,意思是主线程同时创建三个线程。创建一个新的HTTP请求,维护下列设置:Create a new Http Request and maintain the following settings:(1) Protocol: https(2) Server name:(3) Http request method: GET(4) Http path: /sap/c4c/odata/v1/c4codata/AccountCollection/ - 这就是OData服务的相对路径了(5) Use KeepAlive: do NOT select this checkbox - 记得这个勾别打上In Parameter tab, maintain query option $search with value ‘Wang’这个意思就是每个并发请求同时发起OData查询,参数为我的名字WangSwitch to Advanced tab, choose “HttpClient4” from drop down list for Implementation, and maintain proxy server name and port number.如果有代理的话,在下图位置维护代理服务器信息。Create a new HTTP Header Manager and specify the basic authentication header field and value.在HTTP Header Manager里维护访问这个Odata服务的credential。因为我们开发的OData服务支持Basic Authentication这种认真方式,所以我在此处的HTTP header字段里维护Authentication信息。Create a listener for the test plan. In my test I simply choose the most simple one: View Results in Table.创建listener,主要用途当然是显示测试结果了。我使用的是jMeter自带的Listener,Table类型的,以表格形式显示高并发请求和响应的各项指标。Once done, start the test:一切就绪,点击这个绿色的三角形开始测试:After the test is finished, double click on View Result Listener and the response time for each request and the average response time is displayed there:测试完毕后,双击我们之前创建的Table Result Listener,我这三个并发请求的性能指标就显示出来了。可以看到三个请求中,最快的请求用了5.1秒,最慢的6.9秒当然,jMeter也支持命令行方式使用:Or you can use command line to achieve the same:-n: use non-GUI mode-t: specify which test plan you want to run-l: specify the path of output result file为了检验jMeter采集的数据是否正确可靠,我还花时间写了一个Java程序,用JDK自带的线程池产生并发请求,测试的结果和jMeter是一致的。And I have written a simple Java application to generate parallel request via multiple thread and the result measured in Java program is consistent with the one got from JMeter.The source code could be found from my github:我的Java程序放在我的github上:https://github.com/i042416/Ja…How to generate random query for each thread in JMeter到目前为止,我的三个并发请求进行搜索的参数都是硬编码的Wang,这个和实际场景不太符合。有没有办法生成一些随机的搜索字符串,这样更贴近真实使用场景呢?Suppose we would like each thread in JMeter to generate different customer query via OData with the format JerryTestCustomer_<1~100>, we can simply create a new user parameter:当然有办法:右键菜单,Add->Pre Processors(预处理器)->User Parameters:参数名Parameter name,取为uuid参数值Parameter value: use JMeter predefined function __Random to generate random number. 使用jMeter自带的随机数生成函数__Random。因此最后参数uuid的值为${__Random(1,100)},意思是生成1到100内的随机正整数and in http request, just specify reference to this variable via ${uuid}:在http请求里,用固定的前缀JerryTestCustomer_加上随机参数,以此来构造随机搜索字符串:So that in the end each thread will issue different query to OData service end point.通过Table Result listener,能观察到这次确实每个请求发起的搜索都使用了不同的字符串了。希望这篇文章介绍的jMeter使用技巧对大家工作有所帮助。要获取更多Jerry的原创文章,请关注公众号"汪子熙": ...

January 13, 2019 · 2 min · jiezi

Golang并发:除了channle,你还有其他选择

我们都知道Golang并发优选channel,但channel不是万能的,Golang为我们提供了另一种选择:sync。通过这篇文章,你会了解sync包最基础、最常用的方法,至于sync和channel之争留给下一篇文章。sync包提供了基础的异步操作方法,比如互斥锁(Mutex)、单次执行(Once)和等待组(WaitGroup),这些异步操作主要是为低级库提供,上层的异步/并发操作最好选用通道和通信。sync包提供了:Mutex:互斥锁RWMutex:读写锁WaitGroup:等待组Once:单次执行Cond:信号量Pool:临时对象池Map:自带锁的map这篇文章是sync包的入门文章,所以只介绍常用的结构和方法:Mutex、RWMutex、WaitGroup、Once,而Cond、Pool和Map留给大家自行探索,或有需求再介绍。互斥锁常做并发工作的朋友对互斥锁应该不陌生,Golang里互斥锁需要确保的是某段时间内,不能有多个协程同时访问一段代码(临界区)。互斥锁被称为Mutex,它有2个函数,Lock()和Unlock()分别是获取锁和释放锁,如下:type Mutexfunc (m *Mutex) Lock(){}func (m *Mutex) Unlock(){}Mutex的初始值为未锁的状态,并且Mutex通常作为结构体的匿名成员存在。经过了上面这么“官方”的介绍,举个例子:你在工商银行有100元存款,这张卡绑定了支付宝和微信,在中午12点你用支付宝支付外卖30元,你在微信发红包,抢到10块。银行需要按顺序执行上面两件事,先减30再加10或者先加10再减30,结果都是80,但如果同时执行,结果可能是,只减了30或者只加了10,即你有70元或者你有110元。前一个结果是你赔了,后一个结果是银行赔了,银行可不希望把这种事算错。看看实际使用吧:创建一个银行,银行里存每个账户的钱,存储查询都加了锁操作,这样银行就不会算错账了。银行的定义:type Bank struct { sync.Mutex saving map[string]int // 每账户的存款金额}func NewBank() *Bank { b := &Bank{ saving: make(map[string]int), } return b}银行的存取钱:// Deposit 存款func (b *Bank) Deposit(name string, amount int) { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { b.saving[name] = 0 } b.saving[name] += amount}// Withdraw 取款,返回实际取到的金额func (b *Bank) Withdraw(name string, amount int) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } if b.saving[name] < amount { amount = b.saving[name] } b.saving[name] -= amount return amount}// Query 查询余额func (b *Bank) Query(name string) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name]}模拟操作:小米支付宝存了100,并且同时花了20。func main() { b := NewBank() go b.Deposit(“xiaoming”, 100) go b.Withdraw(“xiaoming”, 20) go b.Deposit(“xiaogang”, 2000) time.Sleep(time.Second) fmt.Printf(“xiaoming has: %d\n”, b.Query(“xiaoming”)) fmt.Printf(“xiaogang has: %d\n”, b.Query(“xiaogang”))}结果:先存后花。➜ sync_pkg git:(master) ✗ go run mutex.goxiaoming has: 80xiaogang has: 2000也可能是:先花后存,因为先花20,因为小明没钱,所以没花出去。➜ sync_pkg git:(master) ✗ go run mutex.goxiaoming has: 100xiaogang has: 2000这个例子只是介绍了mutex的基本使用,如果你想多研究下mutex,那就去我的Github(阅读原文)下载下来代码,自己修改测试。Github中还提供了没有锁的例子,运行多次总能碰到错误:fatal error: concurrent map writes这是由于并发访问map造成的。读写锁读写锁是互斥锁的特殊变种,如果是计算机基本知识扎实的朋友会知道,读写锁来自于读者和写者的问题,这个问题就不介绍了,介绍下我们的重点:读写锁要达到的效果是同一时间可以允许多个协程读数据,但只能有且只有1个协程写数据。也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。具体讲,当有至少1个协程读时,如果需要进行写,就必须等待所有已经在读的协程结束读操作,写操作的协程才获得锁进行写数据。当写数据的协程已经在进行时,有其他协程需要进行读或者写,就必须等待已经在写的协程结束写操作。读写锁是RWMutex,它有5个函数,它需要为读操作和写操作分别提供锁操作,这样就4个了:Lock()和Unlock()是给写操作用的。RLock()和RUnlock()是给读操作用的。RLocker()能获取读锁,然后传递给其他协程使用。使用较少。type RWMutexfunc (rw *RWMutex) Lock(){}func (rw *RWMutex) RLock(){}func (rw *RWMutex) RLocker() Locker{}func (rw *RWMutex) RUnlock(){}func (rw *RWMutex) Unlock(){}上面的银行实现不合理:大家都是拿手机APP查余额,可以同时几个人一起查呀,这根本不影响,银行的锁可以换成读写锁。存、取钱是写操作,查询金额是读操作,代码修改如下,其他不变:type Bank struct { sync.RWMutex saving map[string]int // 每账户的存款金额}// Query 查询余额func (b *Bank) Query(name string) int { b.RLock() defer b.RUnlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name]}func main() { b := NewBank() go b.Deposit(“xiaoming”, 100) go b.Withdraw(“xiaoming”, 20) go b.Deposit(“xiaogang”, 2000) time.Sleep(time.Second) print := func(name string) { fmt.Printf("%s has: %d\n", name, b.Query(name)) } nameList := []string{“xiaoming”, “xiaogang”, “xiaohong”, “xiaozhang”} for _, name := range nameList { go print(name) } time.Sleep(time.Second)}结果,可能不一样,因为协程都是并发执行的,执行顺序不固定:➜ sync_pkg git:(master) ✗ go run rwmutex.goxiaohong has: 0xiaozhang has: 0xiaogang has: 2000xiaoming has: 100等待组互斥锁和读写锁大多数人可能比较熟悉,而对等待组(WaitGroup)可能就不那么熟悉,甚至有点陌生,所以先来介绍下等待组在现实中的例子。你们团队有5个人,你作为队长要带领大家打开藏有宝藏的箱子,但这个箱子需要4把钥匙才能同时打开,你把寻找4把钥匙的任务,分配给4个队员,让他们分别去寻找,而你则守着宝箱,在这等待,等他们都找到回来后,一起插进钥匙打开宝箱。这其中有个很重要的过程叫等待:等待一些工作完成后,再进行下一步的工作。如果使用Golang实现,就得使用等待组。等待组是WaitGroup,它有3个函数:Add():在被等待的协程启动前加1,代表要等待1个协程。Done():被等待的协程执行Done,代表该协程已经完成任务,通知等待协程。Wait(): 等待其他协程的协程,使用Wait进行等待。type WaitGroupfunc (wg *WaitGroup) Add(delta int){}func (wg *WaitGroup) Done(){}func (wg *WaitGroup) Wait(){}来,一起看下怎么用WaitGroup实现上面的问题。队长先创建一个WaitGroup对象wg,每个队员都是1个协程, 队长让队员出发前,使用wg.Add(),队员出发寻找钥匙,队长使用wg.Wait()等待(阻塞)所有队员完成,某个队员完成时执行wg.Done(),等所有队员找到钥匙,wg.Wait()则返回,完成了等待的过程,接下来就是开箱。结合之前的协程池的例子,修改成WG等待协程池协程退出,实例代码:func leader() { var wg sync.WaitGroup wg.Add(4) for i := 0; i < 4; i++ { go follower(&wg, i) } wg.Wait() fmt.Println(“open the box together”)}func follower(wg *sync.WaitGroup, id int) { fmt.Printf(“follwer %d find key\n”, id) wg.Done()}结果:➜ sync_pkg git:(master) ✗ go run waitgroup.gofollwer 3 find keyfollwer 1 find keyfollwer 0 find keyfollwer 2 find keyopen the box togetherWaitGroup也常用在协程池的处理上,协程池等待所有协程退出,把上篇文章《Golang并发模型:轻松入门协程池》的例子改下:package mainimport ( “fmt” “sync”)func main() { var once sync.Once onceBody := func() { fmt.Println(“Only once”) } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }}单次执行在程序执行前,通常需要做一些初始化操作,但触发初始化操作的地方是有多处的,但是这个初始化又只能执行1次,怎么办呢?使用Once就能轻松解决,once对象是用来存放1个无入参无返回值的函数,once可以确保这个函数只被执行1次。type Oncefunc (o *Once) Do(f func()){}直接把官方代码给大家搬过来看下,once在10个协程中调用,但once中的函数onceBody()只执行了1次:package mainimport ( “fmt” “sync”)func main() { var once sync.Once onceBody := func() { fmt.Println(“Only once”) } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }}结果:➜ sync_pkg git:(master) ✗ go run once.goOnly once示例源码本文所有示例源码,及历史文章、代码都存储在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg下期预告这次先介绍入门的知识,下次再介绍一些深入思考、最佳实践,不能一口吃个胖子,咱们慢慢来,顺序渐进。下一篇我以这些主题进行介绍,欢迎关注:哪个协程先获取锁一定要用锁吗锁与通道的选择文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门selectGolang并发模型:select进阶Golang并发模型:轻松入门协程池Golang并发的次优选择:sync包如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/ ...

January 5, 2019 · 3 min · jiezi

Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

Java 中15种锁的介绍在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下:公平锁 / 非公平锁可重入锁 / 不可重入锁独享锁 / 共享锁互斥锁 / 读写锁乐观锁 / 悲观锁分段锁偏向锁 / 轻量级锁 / 重量级锁自旋锁上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。公平锁 / 非公平锁公平锁公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。可重入锁 / 不可重入锁可重入锁广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁synchronized void setA() throws Exception{ Thread.sleep(1000); setB();}synchronized void setB() throws Exception{ Thread.sleep(1000);}上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。不可重入锁不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下import java.util.concurrent.atomic.AtomicReference;public class UnreentrantLock { private AtomicReference<Thread> owner = new AtomicReference<Thread>(); public void lock() { Thread current = Thread.currentThread(); //这句是很经典的“自旋”语法,AtomicInteger中也有 for (;;) { if (!owner.compareAndSet(null, current)) { return; } } } public void unlock() { Thread current = Thread.currentThread(); owner.compareAndSet(current, null); }}代码也比较简单,使用原子引用来存放线程,同一线程两次调用lock()方法,如果不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程不必每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。把它变成一个可重入锁:import java.util.concurrent.atomic.AtomicReference;public class UnreentrantLock { private AtomicReference<Thread> owner = new AtomicReference<Thread>(); private int state = 0; public void lock() { Thread current = Thread.currentThread(); if (current == owner.get()) { state++; return; } //这句是很经典的“自旋”式语法,AtomicInteger中也有 for (;;) { if (!owner.compareAndSet(null, current)) { return; } } } public void unlock() { Thread current = Thread.currentThread(); if (current == owner.get()) { if (state != 0) { state–; } else { owner.compareAndSet(current, null); } } }}在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。ReentrantLock中可重入锁实现这里看非公平锁的锁获取方法:final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //就是这里 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; } return false;}在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。独享锁 / 共享锁独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,它俩一个是独享一个是共享锁。独享锁:该锁每一次只能被一个线程所持有。共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。互斥锁 / 读写锁互斥锁在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源读写锁读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态读写锁在Java中的具体实现就是ReadWriteLock一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。乐观锁 / 悲观锁悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。分段锁分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能。在锁上发生竞争时将通水导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。我们一般有三种方式降低锁的竞争程度: 1、减少锁的持有时间 2、降低锁的请求频率 3、使用带有协调机制的独占锁,这些机制允许更高的并发性。在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。其实说的简单一点就是:容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。偏向锁 / 轻量级锁 / 重量级锁锁的状态:无锁状态偏向锁状态轻量级锁状态重量级锁状态锁的状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。偏向锁偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。自旋锁我们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,所以这里给大家讲一下什么是自旋锁。简单回顾一下CAS算法CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数需要读写的内存值 V进行比较的值 A拟写入的新值 B更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。什么是自旋锁?自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。Java如何实现自旋锁?下面是个简单的例子:public class SpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); public void lock() { Thread current = Thread.currentThread(); // 利用CAS while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread current = Thread.currentThread(); cas.compareAndSet(current, null); }}lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。自旋锁存在的问题1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。自旋锁的优点1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)可重入的自旋锁和不可重入的自旋锁文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。public class ReentrantSpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); private int count; public void lock() { Thread current = Thread.currentThread(); if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回 count++; return; } // 如果没获取到锁,则通过CAS自旋 while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟 count–; } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。 cas.compareAndSet(cur, null); } } }}自旋锁与互斥锁自旋锁与互斥锁都是为了实现保护资源共享的机制。无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。自旋锁总结自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。自旋锁本身无法保证公平性,同时也无法保证可重入性。基于自旋锁,可以实现具备公平性和可重入性质的锁。 ...

January 4, 2019 · 2 min · jiezi

Java 并发方案全面学习总结

并发与并行的概念并发(Concurrency): 问题域中的概念—— 程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件并行(Parallelism): 方法域中的概念——通过将问题中的多个部分 并行执行,来加速解决问题。进程、线程与协程它们都是并行机制的解决方案。进程: 进程是什么呢?直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。启动一个进程非常消耗资源,一般一台机器最多启动数百个进程。线程: 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。在进程内启动线程也要消耗一定的资源,一般一个进程最多启动数千个线程。操作系统能够调度的最小单位就是线程了。协程: 协程又从属于线程,它不属于操作系统管辖,完全由程序控制,一个线程内可以启动数万甚至数百万协程。但也正是因为它由程序控制,它对编写代码的风格改变也最多。Java的并行执行实现JVM中的线程主线程: 独立生命周期的线程守护线程: 被主线程创建,随着创建线程结束而结束线程状态要注意的是,线程不是调用start之后马上进入运行中的状态,而是在"可运行"状态,由操作系统来决定调度哪个线程来运行。Jetty中的线程Web服务器都有自己管理的线程池, 比如轻量级的Jetty, 就有以下三种类型的线程:AcceptorSelectorWorker最原始的多线程——Thread类继承类 vs 实现接口继承Thread类实现Runnable接口实际使用中显然实现接口更好, 避免了单继承限制。Runnable vs CallableRunnable:实现run方法,无法抛出受检查的异常,运行时异常会中断主线程,但主线程无法捕获,所以子线程应该自己处理所有异常Callable:实现call方法,可以抛出受检查的异常,可以被主线程捕获,但主线程无法捕获运行时异常,也不会被打断。需要返回值的话,就用Callable接口一个实现了Callable接口的对象,需要被包装为RunnableFuture对象, 然后才能被新线程执行, 而RunnableFuture其实还是实现了Runnable接口。Future, Runnable 和FutureTask的关系如下:可以看出FutureTask其实是RunnableFuture接口的实现类,下面是使用Future的示例代码public class Callee implements Callable { AtomicInteger counter = new AtomicInteger(0); private Integer seq=null; public Callee() { super(); } public Callee(int seq) { this.seq = seq; } /** * call接口可以抛出受检查的异常 * @return * @throws InterruptedException / @Override public Person call() throws InterruptedException { Person p = new Person(“person”+ counter.incrementAndGet(), RandomUtil.random(0,150)); System.out.println(“In thread("+seq+”), create a Person: “+p.toString()); Thread.sleep(1000); return p; }}Callee callee1 = new Callee();FutureTask<Person> ft= new FutureTask<Person>(callee1);Thread thread = new Thread(ft);thread.start();try { thread.join();} catch (InterruptedException e) { e.printStackTrace(); return;}System.out.println(“ft.isDone: “+ft.isDone());Person result1;try { result1 = ((Future<Person>) ft).get();} catch (InterruptedException e) { e.printStackTrace(); result1 = null;} catch (ExecutionException e) { e.printStackTrace(); result1 = null;}Person result = result1;System.out.println(“main thread get result: “+result.toString());线程调度Thread.yield() 方法:调用这个方法,会让当前线程退回到可运行状态,而不是阻塞状态,这样就留给其他同级线程一些运行机会Thread.sleep(long millis):调用这个方法,真的会让当前线程进入阻塞状态,直到时间结束线程对象的join():这个方法让当前线程进入阻塞状态,直到要等待的线程结束。线程对象的interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!Object类中的wait():线程进入等待状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个状态跟加锁有关,所以是Object的方法。Object类中的notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。同步与锁内存一致性错误public class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c–; } public int value() { return c; }}volatilepublic class Foo { private int x = -1; private volatile boolean v = false; public void setX(int x) { this.x = x; v = true; } public int getX() { if (v == true) { return x; } return 0; }}volatile关键字实际上指定了变量不使用寄存器, 并且对变量的访问不会乱序执行。但仅仅对原始类型变量本身生效,如果是++或者–这种“非原子”操作,则不能保证多线程操作的正确性了原子类型JDK提供了一系列对基本类型的封装,形成原子类型(Atomic Variables),特别适合用来做计数器import java.util.concurrent.atomic.AtomicInteger;class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); }}原子操作的实现原理,在Java8之前和之后不同Java7public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; }}Java8public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}至于Compare-and-Swap,以及Fetch-and-Add两种算法,是依赖机器底层机制实现的。线程安全的集合类BlockingQueue: 定义了一个先进先出的数据结构,当你尝试往满队列中添加元素,或者从空队列中获取元素时,将会阻塞或者超时ConcurrentMap: 是 java.util.Map 的子接口,定义了一些有用的原子操作。移除或者替换键值对的操作只有当 key 存在时才能进行,而新增操作只有当 key 不存在时。使这些操作原子化,可以避免同步。ConcurrentMap 的标准实现是 ConcurrentHashMap,它是 HashMap 的并发模式。ConcurrentNavigableMap: 是 ConcurrentMap 的子接口,支持近似匹配。ConcurrentNavigableMap 的标准实现是 ConcurrentSkipListMap,它是 TreeMap 的并发模式。ThreadLocal-只有本线程才能访问的变量ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。synchronized关键字方法加锁:其实不是加在指定的方法上,而是在指定的对象上,只不过在方法开始前会检查这个锁静态方法锁:加在类上,它和加在对象上的锁互补干扰代码区块锁:其实不是加在指定的代码块上,而是加在指定的对象上,只不过在代码块开始前会检查这个锁。一个对象只会有一个锁,所以代码块锁和实例方法锁是会互相影响的需要注意的是:无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问,每个对象只有一个锁(lock)与之相关联加锁不慎可能会造成死锁线程池(Java 5)用途真正的多线程使用,是从线程池开始的,Callable接口,基本上也是被线程池调用的。线程池全景图线程池的使用 ExecutorService pool = Executors.newFixedThreadPool(3); Callable<Person> worker1 = new Callee(); Future ft1 = pool.submit(worker1); Callable<Person> worker2 = new Callee(); Future ft2 = pool.submit(worker2); Callable<Person> worker3 = new Callee(); Future ft3 = pool.submit(worker3); System.out.println(“准备通知线程池shutdown…”); pool.shutdown(); System.out.println(“已通知线程池shutdown”); try { pool.awaitTermination(2L, TimeUnit.SECONDS); System.out.println(“线程池完全结束”); } catch (InterruptedException e) { e.printStackTrace(); }线程池要解决的问题任务排队:当前能并发执行的线程数总是有限的,但任务数可以很大线程调度:线程的创建是比较消耗资源的,需要一个池来维持活跃线程结果收集:每个任务完成以后,其结果需要统一采集线程池类型newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。线程池状态线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。这个会后面线程池的原理会详细分析。但是线程池是出于运行状态,随时准备接受任务来执行。线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。一旦shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。isTerminating()描述的是SHUTDOWN和STOP两种状态。isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。任务拒绝策略Fork/Join模型(Java7)用途计算密集型的任务,最好很少有IO等待,也没有Sleep之类的,最好是本身就适合递归处理的算法分析在给定的线程数内,尽可能地最大化利用CPU资源,但又不会导致其他资源过载(比如内存),或者大量空线程等待。ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。以上程序的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。ps:ForkJoinPool在执行过程中,会创建大量的子任务,导致GC进行垃圾回收,这些是需要注意的。原理与使用ForkJoinPool首先是ExecutorService的实现类,因此是特殊的线程池。创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务了。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。个人认为ForkJoinPool设计不太好的地方在于,ForkJoinTask不是个接口,而是抽象类,实际使用时基本上不是继承RecursiveAction就是继承RecursiveTask,对业务类有限制。示例典型的一个例子,就是一串数组求和public interface Calculator { long sumUp(long[] numbers);}public class ForkJoinCalculator implements Calculator { private ForkJoinPool pool; private static class SumTask extends RecursiveTask<Long> { private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) { this.numbers = numbers; this.from = from; this.to = to; } @Override protected Long compute() { // 当需要计算的数字小于6时,直接计算结果 if (to - from < 6) { long total = 0; for (int i = from; i <= to; i++) { total += numbers[i]; } return total; // 否则,把任务一分为二,递归计算 } else { int middle = (from + to) / 2; SumTask taskLeft = new SumTask(numbers, from, middle); SumTask taskRight = new SumTask(numbers, middle+1, to); taskLeft.fork(); taskRight.fork(); return taskLeft.join() + taskRight.join(); } } } public ForkJoinCalculator() { // 也可以使用公用的 ForkJoinPool: // pool = ForkJoinPool.commonPool() pool = new ForkJoinPool(); } @Override public long sumUp(long[] numbers) { return pool.invoke(new SumTask(numbers, 0, numbers.length-1)); }}这个例子展示了当数组被拆分得足够小(<6)之后,就不需要并行处理了,而更大的数组就拆为两半,分别处理。Stream(Java 8)概念别搞混了,跟IO的Stream完全不是一回事,可以把它看做是集合处理的声明式语法,类似数据库操作语言SQL。当然也有跟IO类似的地方,就是Stream只能消费一次,不能重复使用。看个例子:int sum = widgets.stream().filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum();流提供了一个能力,任何一个流,只要获取一次并行流,后面的操作就都可以并行了。例如:Stream<String> stream = Stream.of(“a”, “b”, “c”,“d”,“e”,“f”,“g”);String str = stream.parallel().reduce((a, b) -> a + “,” + b).get();System.out.println(str);流操作生成流Collection.stream()Collection.parallelStream()Arrays.stream(T array) or Stream.of()java.io.BufferedReader.lines()java.util.stream.IntStream.range()java.nio.file.Files.walk()java.util.SpliteratorRandom.ints()BitSet.stream()Pattern.splitAsStream(java.lang.CharSequence)JarFile.stream()示例// 1. Individual valuesStream stream = Stream.of(“a”, “b”, “c”);// 2. ArraysString [] strArray = new String[] {“a”, “b”, “c”};stream = Stream.of(strArray);stream = Arrays.stream(strArray);// 3. CollectionsList<String> list = Arrays.asList(strArray);stream = list.stream();需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。Intermediate一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。已知的Intermediate操作包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered。Terminal一个流只能有一个 terminal操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。已知的Terminal操作包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iteratorreduce解析: reduce本质上是个聚合方法,它的作用是用流里面的元素生成一个结果,所以用来做累加,字符串拼接之类的都非常合适。它有三个参数初始值:最终结果的初始化值,可以是一个空的对象聚合函数:一个二元函数(有两个参数),第一个参数是上一次聚合的结果,第二个参数是某个元素多个部分结果的合并函数:如果流并发了,那么聚合操作会分为多段进行,这里显示了多段之间如何配合collect: collect比reduce更强大:reduce最终只能得到一个跟流里数据类型相同的值, 但collect的结果可以是任何对象。简单的collect也有三个参数:最终要返回的数据容器把元素并入返回值的方法多个部分结果的合并两个collect示例//和reduce相同的合并字符操作String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();//等价于上面,这样看起来应该更加清晰String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();//把stream转成mapStream stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));/ 或者使用方法引用 */result = stream.collect(ArrayList::new, List::add, List::addAll);协程协程,英文Coroutines,也叫纤程(Fiber)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程实际上是在语言底层(或者框架)对需要等待的程序进行调度,从而充分利用CPU的方法, 其实这完全可以通过回调来实现, 但是深层回调的代码太变态了,所以发明了协程的写法。理论上多个协程不会真的"同时"执行,也就不会引起共享变量操作的不确定性,不需要加锁(待确认)。pythone协程示例Pythone, Golang和C#都内置了协程的语法,但Java没有,只能通过框架实现,常见的框架包括:Quasar,kilim和ea-async。Java ea-async 协程示例import static com.ea.async.Async.await;import static java.util.concurrent.CompletableFuture.completedFuture;public class Store{ //购物操作, 传一个商品id和一个价格 public CompletableFuture<Boolean> buyItem(String itemTypeId, int cost) { //银行扣款(长时间操作) if(!await(bank.decrement(cost))) { return completedFuture(false); } try { //商品出库(长时间操作) await(inventory.giveItem(itemTypeId)); return completedFuture(true); } catch (Exception ex) { await(bank.refund(cost)); throw new AppException(ex); } }}参考资料《七周七并发模型》电子书深入浅出Java Concurrency——线程池Java多线程学习(吐血超详细总结)Jetty基础之线程模型Jetty-server高性能,多线程特性的源码分析Java 编程要点之并发(Concurrency)详解Java Concurrency in Depth (Part 1)Java进阶(七)正确理解Thread Local的原理与适用场景Java 并发编程笔记:如何使用 ForkJoinPool 以及原理ForkJoinPool简介多线程 ForkJoinPoolJava 8 中的 Streams API 详解Java中的协程实现漫画:什么是协程学习源码 ...

January 4, 2019 · 4 min · jiezi

简说Java线程的那几个启动方式

本文首发于 猫叔的博客,转载请申明出处前言并发是一件很美妙的事情,线程的调度与使用会让你除了业务代码外,有新的世界观,无论你是否参与但是这对于你未来的成长帮助很大。所以,让我们来好好看看在Java中启动线程的那几个方式与介绍。Thread对于 Thread 我想这个基本上大家都认识的,在Java源码是这样说: java 虚拟机允许应用程序同时运行多个执行线程。 而这个的 Thread 就是程序的执行线程。如何使用它呢,其实在这个类中的源码已经给我们写好了,甚至是下面的 Runnable 的使用方式。(如下是Thread源码)/** * A <i>thread</i> is a thread of execution in a program. The Java * Virtual Machine allows an application to have multiple threads of * execution running concurrently. * <hr><blockquote><pre> * class PrimeThread extends Thread { * long minPrime; * PrimeThread(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * &nbsp;.&nbsp;.&nbsp;. * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeThread p = new PrimeThread(143); * p.start(); * </pre></blockquote> * <p> * <hr><blockquote><pre> * class PrimeRun implements Runnable { * long minPrime; * PrimeRun(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * &nbsp;.&nbsp;.&nbsp;. * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeRun p = new PrimeRun(143); * new Thread(p).start(); * </pre></blockquote> * <p> /public class Thread implements Runnable { //…}阅读源码的信息其实是最全的 ,我截取了部分的注释信息,起码我们现在可以无压力的使用这个两个方式来启动自己的线程。如果我们还要传递参数的话,那么我们设定一个自己的构造函数也是可以,如下方式:public class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { System.out.println(“一个子线程 BY " + getName()); }}这时读者应该发现,这个构造函数中的 name ,居然在 Thread 中也是有的,其实在Java中的线程都会自己的名称,如果我们不给其定义名称的话,java也会自己给其命名。/** Allocates a new {@code Thread} object. This constructor has the same* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}* {@code (null, null, name)}.** @param name* the name of the new thread*/public Thread(String name) { init(null, null, name, 0);}而我们最核心,也是大家最在意的应该就是如何启动并执行我们的线程了,是的,这个大家都知道的,就是这个 run 方法了。同时大家如果了解过了 Runnable ,我想大家都会知道这个 run 方法,其实是 Runnable 的方法,而我们本节的 Thread 也是实现了这个接口。这里,大家可能会好奇,不是应该是 start 这个方法吗?那么让我们看看 start 的源码。/** * Causes this thread to begin execution; the Java Virtual Machin * calls the <code>run</code> method of this thread. /public synchronized void start() { //…}通过 start 方法,我们可以了解到,就如同源码的启动模板中那样,官网希望,对于线程的启动,使用者是通过 start 的方式来启动线程,因为这个方法会让Java虚拟机会调用这个线程的 run 方法。其结果就是,一个线程去运行 start 方法,而另一个线程则取运行 run 方法。同时对于这样线程,Java官方也说了,线程是不允许多次启动的,这是不合法的。所以如果我们执行下面的代码,就会报 java.lang.IllegalThreadStateException 异常。MyThread myThread = new MyThread(“Thread”);myThread.start();myThread.start();但是,如果是这样的代码呢?MyThread myThread = new MyThread(“Thread”);myThread.run();myThread.run();myThread.start();//运行结果一个子线程 BY Thread一个子线程 BY Thread一个子线程 BY Thread这是不合理的,如果大家有兴趣,可以去试试并动手测试下,最好开调试模式。下面我们再看看,连 Thread 都要实现,且核心的 run 方法出处的 Runnable 。Runnable比起 Thread 我希望大家跟多的使用 Runnable 这个接口实现的方式,对于好坏对比会在总结篇说下。我想大家看 Runnable 的源码会更加容易与容易接受,毕竟它有一个 run 方法。(如下为其源码)/* * The <code>Runnable</code> interface should be implemented by any * class whose instances are intended to be executed by a thread. The * class must define a method of no arguments called <code>run</code>. /@FunctionalInterfacepublic interface Runnable { /* * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object’s * <code>run</code> method to be called in that separately executing * thread. / public abstract void run();}首先,所有打算执行线程的类均可实现这个 Runnable 接口,且必须实现 run 方法。它将为各个类提供一个协议,就像 Thread 一样,其实当我们的类实现了 Runnable 的接口后,我们的类与 Thread 是同级,只是可能仅有 run 方法,而没有 Thread 提供的跟丰富的功能方法。而对于 run 方法,则是所有实现了 Runnable 接口的类,在调用 start 后,将使其单独执行 run 方法。那么我们可以写出这样的测试代码。MyThreadRunnable myThreadRunnable = new MyThreadRunnable(“Runnabel”);myThreadRunnable.run();new Thread(myThreadRunnable).start();Thread thread = new Thread(myThreadRunnable);thread.start();thread.start();//运行效果Exception in thread “main” java.lang.IllegalThreadStateException at java.lang.Thread.start(Thread.java:705) at com.github.myself.runner.RunnableApplication.main(RunnableApplication.java:14)这是一个子线程 BY Runnabel这是一个子线程 BY Runnabel这是一个子线程 BY Runnabel同样的,线程是不允许多次启动的,这是不合法的。同时,这时我们也看出了使用 Thread 与 Runnable 的区别,当我们要多次启用一个相同的功能时。我想 Runnable 更适合你。但是,用了这两个方式,我们要如何知道线程的运行结果呢???FutureTask这个可能很少人(初学者)用到,不过这个现在是我最感兴趣的。它很有趣。其实还有一个小兄弟,那就是 Callable。 它们是一对搭档。如果上面的内容,你已经细细品味过,那么你应该已经发现 Callable 了。没错,他就在 Runnable 的源码中出现过。/* * @author Arthur van Hoff * @see java.lang.Thread * @see java.util.concurrent.Callable * @since JDK1.0 / @FunctionalInterfacepublic interface Runnable {}那么我们先去看看这个 Callable 吧。(如下为其源码)/* * A task that returns a result and may throw an exception. * Implementors define a single method with no arguments called * {@code call}. /@FunctionalInterfacepublic interface Callable<V> { /* * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result / V call() throws Exception;}其实,这是一个与 Runnable 基本相同的接口,当时它可以返回执行结果与检查异常,其计算结果将由 call() 方法返回。那么其实我们现在可以写出一个实现的类。public class MyCallable implements Callable { private String name; public MyCallable(String name) { this.name = name; } @Override public Object call() throws Exception { System.out.println(“这是一个子线程 BY " + name); return “successs”; }}关于更深入的探讨,我将留到下一篇文章中。好了,我想我们应该来看看 FutureTask 这个类的相关信息了。/* * A cancellable asynchronous computation. This class provides a base * implementation of {@link Future}, with methods to start and cancel * a computation, query to see if the computation is complete, and * retrieve the result of the computation. The result can only be * retrieved when the computation has completed; the {@code get} * methods will block if the computation has not yet completed. Once * the computation has completed, the computation cannot be restarted * or cancelled (unless the computation is invoked using * {@link #runAndReset}). / public class FutureTask<V> implements RunnableFuture<V> { //… }源码写的很清楚,这是一个可以取消的异步计算,提供了查询、计算、查看结果等的方法,同时我们还可以使用 runAndRest 来让我们可以重新启动计算。在查看其构造函数的时候,很高兴,我们看到了我们的 Callable 接口。/* * Creates a {@code FutureTask} that will, upon running, execute the * given {@code Callable}. * * @param callable the callable task * @throws NullPointerException if the callable is null */public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable}即我们将创建一个未来任务,来执行 Callable 的实现类。那么我们现在可以写出这样的代码了。final FutureTask fun = new FutureTask(new MyCallable(“Future”));那么接下来我们就可以运行我们的任务了吗?是的,我知道了 run() 方法,但是却没有 start 方法。官方既然说有结果,那么我找到了 get 方法。同时我尝试着写了一下测试代码。public static void main(String[] args) { MyCallable myCallable = new MyCallable(“Callable”); final FutureTask fun = new FutureTask(myCallable); fun.run(); try { Object result = fun.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }}运行效果,是正常的,这好像是那么回事。//运行效果这是一个子线程 BY Callablesuccesss可是,在我尝试着加多一些代码的时候,却发现了一些奇妙的东西 。我加多了一行 fun.run(); 代码,同时在 MyCallable 类中,将方法加一个时间线程去等待3s。结果是: 结果只输出了一次,同时 get 方法需要等运行3s后才有返回。这并不是我希望看到的。但是,起码我们可以知道,这次即使我们多次运行使用 run 方法,但是这个线程也只运行了一次。这是一个好消息。同时,我们也拿到了任务的结果,当时我们的进程被阻塞了,我们需要去等我们的任务执行完成。最后,在一番小研究后,以下的代码终于完成了我们预期的期望。public static void main(String[] args) { MyCallable myCallable = new MyCallable(“Callable”); ExecutorService executorService = Executors.newCachedThreadPool(); final FutureTask fun = new FutureTask(myCallable); executorService.execute(fun);// fun.run(); //阻塞进程 System.out.println(”–继续执行”); try { Object result = fun.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }}我们使用线程池去运行我们的 FutureTask 同时使用 get 方法去获取运行后的结果。结果是友好的,进程并不会被阻塞。关于更深入的探讨,我将留到下一篇文章中。总结一波好了,现在应该来整理以下了。Thread 需要我们继承实现,这是比较局限的,因为Java的 继承资源 是有限的,同时如果多次执行任务,还需要 多次创建任务类。Runnable 以接口的形式让我们实现,较为方便,同时多次执行任务也无需创建多个任务类,当时仅有一个 run 方法。以上两个方法都 无法获取任务执行结果 ,FutureTask可以获取任务结果。同时还有更多的新特性方便我们使用···公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

December 30, 2018 · 5 min · jiezi

Golang并发:一招掌握无阻塞通道读写

介绍Golang并发的模型写了几篇了,但一直没有以channel为主题进行介绍,今天就给大家聊一聊channel,channel的基本使用非常简单,想必大家都已了解,所以直接来个进阶点的:介绍channel的阻塞情况,以及给你一个必杀技,立马解决阻塞问题,实用性高。阻塞场景无论是有缓存通道、无缓冲通道都存在阻塞的情况。阻塞场景共4个,有缓存和无缓冲各2个。无缓冲通道的特点是,发送的数据需要被读取后,发送才会完成,它阻塞场景:通道中无数据,但执行读通道。通道中无数据,向通道写数据,但无协程读取。// 场景1func ReadNoDataFromNoBufCh() { noBufCh := make(chan int) <-noBufCh fmt.Println(“read from no buffer channel success”) // Output: // fatal error: all goroutines are asleep - deadlock!}// 场景2func WriteNoBufCh() { ch := make(chan int) ch <- 1 fmt.Println(“write success no block”) // Output: // fatal error: all goroutines are asleep - deadlock!}注:示例代码中的Output注释代表函数的执行结果,每一个函数都由于阻塞在通道操作而无法继续向下执行,最后报了死锁错误。有缓存通道的特点是,有缓存时可以向通道中写入数据后直接返回,缓存中有数据时可以从通道中读到数据直接返回,这时有缓存通道是不会阻塞的,它阻塞场景是:通道的缓存无数据,但执行读通道。通道的缓存已经占满,向通道写数据,但无协程读。// 场景1func ReadNoDataFromBufCh() { bufCh := make(chan int, 1) <-bufCh fmt.Println(“read from no buffer channel success”) // Output: // fatal error: all goroutines are asleep - deadlock!}// 场景2func WriteBufChButFull() { ch := make(chan int, 1) // make ch full ch <- 100 ch <- 1 fmt.Println(“write success no block”) // Output: // fatal error: all goroutines are asleep - deadlock!}使用Select实现无阻塞读写select是执行选择操作的一个结构,它里面有一组case语句,它会执行其中无阻塞的那一个,如果都阻塞了,那就等待其中一个不阻塞,进而继续执行,它有一个default语句,该语句是永远不会阻塞的,我们可以借助它实现无阻塞的操作。如果不了解,不想多了解一下select可以先看下这2篇文章:Golang并发模型:轻松入门selectGolang并发模型:select进阶下面示例代码是使用select修改后的无缓冲通道和有缓冲通道的读写,以下函数可以直接通过main函数调用,其中的Ouput的注释是运行结果,从结果能看出,在通道不可读或者不可写的时候,不再阻塞等待,而是直接返回。// 无缓冲通道读func ReadNoDataFromNoBufChWithSelect() { bufCh := make(chan int) if v, err := ReadWithSelect(bufCh); err != nil { fmt.Println(err) } else { fmt.Printf(“read: %d\n”, v) } // Output: // channel has no data}// 有缓冲通道读func ReadNoDataFromBufChWithSelect() { bufCh := make(chan int, 1) if v, err := ReadWithSelect(bufCh); err != nil { fmt.Println(err) } else { fmt.Printf(“read: %d\n”, v) } // Output: // channel has no data}// select结构实现通道读func ReadWithSelect(ch chan int) (x int, err error) { select { case x = <-ch: return x, nil default: return 0, errors.New(“channel has no data”) }}// 无缓冲通道写func WriteNoBufChWithSelect() { ch := make(chan int) if err := WriteChWithSelect(ch); err != nil { fmt.Println(err) } else { fmt.Println(“write success”) } // Output: // channel blocked, can not write}// 有缓冲通道写func WriteBufChButFullWithSelect() { ch := make(chan int, 1) // make ch full ch <- 100 if err := WriteChWithSelect(ch); err != nil { fmt.Println(err) } else { fmt.Println(“write success”) } // Output: // channel blocked, can not write}// select结构实现通道写func WriteChWithSelect(ch chan int) error { select { case ch <- 1: return nil default: return errors.New(“channel blocked, can not write”) }}使用Select+超时改善无阻塞读写使用default实现的无阻塞通道阻塞有一个缺陷:当通道不可读或写的时候,会即可返回。实际场景,更多的需求是,我们希望尝试读一会数据,或者尝试写一会数据,如果实在没法读写再返回,程序继续做其它的事情。使用定时器替代default可以解决这个问题,给通道增加读写数据的容忍时间,如果500ms内无法读写,就即刻返回。示例代码修改一下会是这样:func ReadWithSelect(ch chan int) (x int, err error) { timeout := time.NewTimer(time.Microsecond * 500) select { case x = <-ch: return x, nil case <-timeout.C: return 0, errors.New(“read time out”) }}func WriteChWithSelect(ch chan int) error { timeout := time.NewTimer(time.Microsecond * 500) select { case ch <- 1: return nil case <-timeout.C: return errors.New(“write time out”) }}结果就会变成超时返回:read time outwrite time outread time outwrite time out示例源码本文所有示例源码,及历史文章、代码都存储在Github:https://github.com/Shitaibin/…这篇文章了channel的阻塞情况,以及解决阻塞的2种办法:使用select的default语句,在channel不可读写时,即可返回使用select+定时器,在超时时间内,channel不可读写,则返回希望这篇文章对你的channel读写有所启发。如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2018/11/03/Golang-channel-read-and-write-without-blocking/ ...

December 27, 2018 · 2 min · jiezi

Java并发编程——线程安全性深层原因

线程安全性深层原因这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因。缓存一致性问题CPU内存架构随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的访问速度非常快,而内存访问速度相对偏慢),所有在CPU和内存之间出现了多级高速缓存。下图是现代CPU和内存的一般架构图:我们可以看到高速缓存也分为三级缓存,越靠近寄存器的级别缓存访问速度越快。其中L3 Cache为多核共享的,L1和L2 Cache为单核独享,而L1又有数据缓存(L1 d)和指令缓存(L1 i)。正因为高速缓存的出现,各CPU内核从主内存获取相同的数据将会存在于缓存中,当多核都对此数据进行操作并修改值,此时另外的核心并不知道此值已被其他核心修改,从而出现缓存不一致的问题。如何解决缓存一致性问题解决缓存一致性问题一般有两个方法:第一个是采用总线锁,在总线级别加锁,这样从内存种访问到的数据将被当个CPU核心独占,在多核的情况下对单个资源将是串行化的。这种方式性能上将大打折扣。第二个是采用缓存锁,在缓存的级别上进行加锁。此种方式需要某种协议对缓存行数据进行同步,后面所说的缓存一致行协议便是一种实现。缓存一致性协议(MESI)为了解决缓存一致性的问题,一些CPU系列(比如Intel奔腾系列)采用了MESI协议来解决缓存一致性问题。此协议将每个缓存行(Cache Line)使用4种状态进行标记。M: 被修改(Modified)该缓存行只被缓存在该CPU核心的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。E: 独享的(Exclusive)该缓存行只被缓存在该CPU核心缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU核心读取该内存时变成共享状态(shared)。同样地,当CPU核心修改该缓存行中内容时,该状态可以变成Modified状态。S: 共享的(Shared)该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。I: 无效的(Invalid)该缓存是无效的(可能有其它CPU核心修改了该缓存行)在MESI协议中,每个CPU核心的缓存控制器不仅知道自己的操作(local read和local write),每个核心的缓存控制器通过监听也知道其他CPU中cache的操作(remote read和remote write),再确定自己cache中共享数据的状态是否需要调整。local read(LR):读本地cache中的数据;local write(LW):将数据写到本地cache;remote read(RR):其他核心发生read;remote write(RW):其他核心发生write;针对操作,缓存行的状态迁移图如下:指令重排序问题在我们编程过程中,习惯性程序思维认为程序是按我们写的代码顺序执行的,举个例子来说,某个程序中有三行代码:int a = 1; // 1int b = 2; // 2int c = a + b; // 3从程序员角度执行顺序应该是1 -> 2 -> 3,实际经过编译器和CPU的优化很有可能执行顺序会变成 2 -> 1 -> 3(注意这样的优化重排并没有改变最终的结果)。类似这种不影响单线程语义的乱序执行我们称为指令重排。(后面讲Java内存模型也会讲到这部分。)编译器指令重排举个例子,我们先看可以看一段代码:class ReorderExample { int a = 0; boolean flag = false; public void write() { a = 1; // 1 flag = true; // 2 } public void read() { if (flag) { // 3 int i = a * a; // 4 } }}在单线程的情况下如果先write再read的话,i的结果应该是1。但是在多线程的情况下,编译器很可能对指令进行重排,有可能出现的执行顺序是2 -> 3 -> 4 -> 1。这个时候的i的结果就是0了。(1和2之间以及3和4之间不存在数据依赖,有关数据依赖在后面的Java内存模型中会讲到。)CPU指令重排在CPU层面,一条指令被分为多个步骤来执行,每个步骤会使用不同的硬件(比如寄存器、存储器、算术逻辑单元等)。执行多个指令时采用流水线技术进行执行,如下示意图:注意这里出现的”停顿“,出现这个原因是因为步骤22需要步骤13得到结果后才能进行。CPU为了进一般优化:消除一些停顿,这时会将指令3(指令3对指令2和1都没有数据依赖)移到指令2之前进行运行。这样就出现了指令重排,根本原因是为了优化指令的执行。内存系统重排CPU经过长时间的优化,在寄存器和L1缓存之间添加了LoadBuffer、StoreBuffer来降低阻塞时间。LoadBuffer、StoreBuffer,合称排序缓冲(Memoryordering Buffers (MOB)),Load缓冲64长度,store缓冲36长度,Buffer与L1进行数据传输时,CPU无须等待。CPU执行load读数据时,把读请求放到LoadBuffer,这样就不用等待其它CPU响应,先进行下面操作,稍后再处理这个读请求的结果。CPU执行store写数据时,把数据写到StoreBuffer中,待到某个适合的时间点,把StoreBuffer的数据刷到主存中。因为StoreBuffer的存在,CPU在写数据时,真实数据并不会立即表现到内存中,所以对于其它CPU是不可见的;同样的道理,LoadBuffer中的请求也无法拿到其它CPU设置的最新数据;由于StoreBuffer和LoadBuffer是异步执行的,所以在外面看来,先写后读,还是先读后写,没有严格的固定顺序。由于引入StoreBuffer和LoadBuffer导致异步模式,从而导致内存数据的读写可能是乱序的(也就是内存系统的重排序)。延伸在程序我们常说的三大性质:可见性、原子性、有序性。通过线程安全性深层原因我们能更好的理解这三大性质的根本性原因。(可见性、原子性、有序性会在后面文章中进行详细讲解。) ...

December 18, 2018 · 1 min · jiezi

Golang并发模型:select进阶

最近公司工作有点多,Golang的select进阶就这样被拖沓啦,今天坚持把时间挤一挤,把吹的牛皮补上。前一篇文章《Golang并发模型:轻松入门select》介绍了select的作用和它的基本用法,这次介绍它的3个进阶特性。nil的通道永远阻塞如何跳出for-selectselect{}阻塞nil的通道永远阻塞当case上读一个通道时,如果这个通道是nil,则该case永远阻塞。这个功能有1个妙用,select通常处理的是多个通道,当某个读通道关闭了,但不想select再继续关注此case,继续处理其他case,把该通道设置为nil即可。下面是一个合并程序等待两个输入通道都关闭后才退出的例子,就使用了这个特性。func combine(inCh1, inCh2 <-chan int) <-chan int { // 输出通道 out := make(chan int) // 启动协程合并数据 go func() { defer close(out) for { select { case x, open := <-inCh1: if !open { inCh1 = nil continue } out<-x case x, open := <-inCh2: if !open { inCh2 = nil continue } out<-x } // 当ch1和ch2都关闭是才退出 if inCh1 == nil && inCh2 == nil { break } } }() return out}如何跳出for-selectbreak在select内的并不能跳出for-select循环。看下面的例子,consume函数从通道inCh不停读数据,期待在inCh关闭后退出for-select循环,但结果是永远没有退出。func consume(inCh <-chan int) { i := 0 for { fmt.Printf(“for: %d\n”, i) select { case x, open := <-inCh: if !open { break } fmt.Printf(“read: %d\n”, x) } i++ } fmt.Println(“combine-routine exit”)}运行结果:➜ go run x.gofor: 0read: 0for: 1read: 1for: 2read: 2for: 3gen exitfor: 4for: 5for: 6for: 7for: 8… // never stop既然break不能跳出for-select,那怎么办呢?给你3个锦囊:在满足条件的case内,使用return,如果有结尾工作,尝试交给defer。在select外for内使用break挑出循环,如combine函数。使用goto。select{}永远阻塞select{}的效果等价于创建了1个通道,直接从通道读数据:ch := make(chan int)<-ch但是,这个写起来多麻烦啊!没select{}简洁啊。但是,永远阻塞能有什么用呢!?当你开发一个并发程序的时候,main函数千万不能在子协程干完活前退出啊,不然所有的协程都被迫退出了,还怎么提供服务呢?比如,写了个Web服务程序,端口监听、后端处理等等都在子协程跑起来了,main函数这时候能退出吗?select应用场景最后,介绍下我常用的select场景:无阻塞的读、写通道。即使通道是带缓存的,也是存在阻塞的情况,使用select可以完美的解决阻塞读写,这篇文章我之前发在了个人博客,后面给大家介绍下。给某个请求/处理/操作,设置超时时间,一旦超时时间内无法完成,则停止处理。select本色:多通道处理并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 18, 2018 · 1 min · jiezi

Spring Boot+SQL/JPA实战悲观锁和乐观锁

最近在公司的业务上遇到了并发的问题,并且还是很常见的并发问题,算是低级的失误了。由于公司业务相对比较复杂且不适合公开,在此用一个很常见的业务来还原一下场景,同时介绍悲观锁和乐观锁是如何解决这类并发问题的。公司业务就是最常见的“订单+账户”问题,在解决完公司问题后,转头一想,我的博客项目Fame中也有同样的问题(虽然访问量根本完全不需要考虑并发问题…),那我就拿这个来举例好了。业务还原首先环境是:Spring Boot 2.1.0 + data-jpa + mysql + lombok数据库设计对于一个有评论功能的博客系统来说,通常会有两个表:1.文章表 2.评论表。其中文章表除了保存一些文章信息等,还有个字段保存评论数量。我们设计一个最精简的表结构来还原该业务场景。article 文章表字段类型备注idINT自增主键idtitleVARCHAR文章标题comment_countINT文章的评论数量comment 评论表字段类型备注idINT自增主键idarticle_idINT评论的文章idcontentVARCHAR评论内容当一个用户评论的时候,1. 根据文章id获取到文章 2. 插入一条评论记录 3. 该文章的评论数增加并保存代码实现首先在maven中引入对应的依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.0.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>然后编写对应数据库的实体类@Data@Entitypublic class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private Long commentCount;}@Data@Entitypublic class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long articleId; private String content;}接着创建这两个实体类对应的Repository,由于spring-jpa-data的CrudRepository已经帮我们实现了最常见的CRUD操作,所以我们的Repository只需要继承CrudRepository接口其他啥都不用做。public interface ArticleRepository extends CrudRepository<Article, Long> {}public interface CommentRepository extends CrudRepository<Comment, Long> {}接着我们就简单的实现一下Controller接口和Service实现类。@Slf4j@RestControllerpublic class CommentController { @Autowired private CommentService commentService; @PostMapping(“comment”) public String comment(Long articleId, String content) { try { commentService.postComment(articleId, content); } catch (Exception e) { log.error("{}", e); return “error: " + e.getMessage(); } return “success”; }}@Slf4j@Servicepublic class CommentService { @Autowired private ArticleRepository articleRepository; @Autowired private CommentRepository commentRepository; public void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); if (!articleOptional.isPresent()) { throw new RuntimeException(“没有对应的文章”); } Article article = articleOptional.get(); Comment comment = new Comment(); comment.setArticleId(articleId); comment.setContent(content); commentRepository.save(comment); article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article); }}并发问题分析从刚才的代码实现里可以看出这个简单的评论功能的流程,当用户发起评论的请求时,从数据库找出对应的文章的实体类Article,然后根据文章信息生成对应的评论实体类Comment,并且插入到数据库中,接着增加该文章的评论数量,再把修改后的文章更新到数据库中,整个流程如下流程图。在这个流程中有个问题,当有多个用户同时并发评论时,他们同时进入步骤1中拿到Article,然后插入对应的Comment,最后在步骤3中更新评论数量保存到数据库。只是由于他们是同时在步骤1拿到的Article,所以他们的Article.commentCount的值相同,那么在步骤3中保存的Article.commentCount+1也相同,那么原来应该+3的评论数量,只加了1。我们用测试用例代码试一下@RunWith(SpringRunner.class)@SpringBootTest(classes = LockAndTransactionApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class CommentControllerTests { @Autowired private TestRestTemplate testRestTemplate; @Test public void concurrentComment() { String url = “http://localhost:9090/comment”; for (int i = 0; i < 100; i++) { int finalI = i; new Thread(() -> { MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add(“articleId”, “1”); params.add(“content”, “测试内容” + finalI); String result = testRestTemplate.postForObject(url, params, String.class); }).start(); } }}这里我们开了100个线程,同时发送评论请求,对应的文章id为1。在发送请求前,数据库数据为select * from articleselect count() comment_count from comment发送请求后,数据库数据为select * from articleselect count() comment_count from comment明显的看到在article表里的comment_count的值不是100,这个值不一定是我图里的14,但是必然是不大于100的,而comment表的数量肯定等于100。这就展示了在文章开头里提到的并发问题,这种问题其实十分的常见,只要有类似上面这样评论功能的流程的系统,都要小心避免出现这种问题。下面就用实例展示展示如何通过悲观锁和乐观锁防止出现并发数据问题,同时给出SQL方案和JPA自带方案,SQL方案可以通用“任何系统”,甚至不限语言,而JPA方案十分快捷,如果你恰好用的也是JPA,那就可以简单的使用上乐观锁或悲观锁。最后也会根据业务比较一下乐观锁和悲观锁的一些区别悲观锁解决并发问题悲观锁顾名思义就是悲观的认为自己操作的数据都会被其他线程操作,所以就必须自己独占这个数据,可以理解为”独占锁“。在java中synchronized和ReentrantLock等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁。利用SQL解决并发问题行锁就是操作数据的时候把这一行数据锁住,其他线程想要读写必须等待,但同一个表的其他数据还是能被其他线程操作的。只要在需要查询的sql后面加上for update,就能锁住查询的行,特别要注意查询条件必须要是索引列,如果不是索引就会变成表锁,把整个表都锁住。现在在原有的代码的基础上修改一下,先在ArticleRepository增加一个手动写sql查询方法。public interface ArticleRepository extends CrudRepository<Article, Long> { @Query(value = “select * from article a where a.id = :id for update”, nativeQuery = true) Optional<Article> findArticleForUpdate(Long id);}然后把CommentService中使用的查询方法由原来的findById改为我们自定义的方法public class CommentService { … public void postComment(Long articleId, String content) { // Optional<Article> articleOptional = articleRepository.findById(articleId); Optional<Article> articleOptional = articleRepository.findArticleForUpdate(articleId); … }}这样我们查出来的Article,在我们没有将其提交事务之前,其他线程是不能获取修改的,保证了同时只有一个线程能操作对应数据。现在再用测试用例测一下,article.comment_count的值必定是100。利用JPA自带行锁解决并发问题对于刚才提到的在sql后面增加for update,JPA有提供一个更优雅的方式,就是@Lock注解,这个注解的参数可以传入想要的锁级别。现在在ArticleRepository中增加JPA的锁方法,其中LockModeType.PESSIMISTIC_WRITE参数就是行锁。public interface ArticleRepository extends CrudRepository<Article, Long> { … @Lock(value = LockModeType.PESSIMISTIC_WRITE) @Query(“select a from Article a where a.id = :id”) Optional<Article> findArticleWithPessimisticLock(Long id);}同样的只要在CommentService里把查询方法改为findArticleWithPessimisticLock(),再测试用例测一下,肯定不会有并发问题。而且这时看一下控制台打印信息,发现实际上查询的sql还是加了for update,只不过是JPA帮我们加了而已。乐观锁解决并发问题乐观锁顾名思义就是特别乐观,认为自己拿到的资源不会被其他线程操作所以不上锁,只是在插入数据库的时候再判断一下数据有没有被修改。所以悲观锁是限制其他线程,而乐观锁是限制自己,虽然他的名字有锁,但是实际上不算上锁,只是在最后操作的时候再判断具体怎么操作。乐观锁通常为版本号机制或者CAS算法利用SQL实现版本号解决并发问题版本号机制就是在数据库中加一个字段当作版本号,比如我们加个字段version。那么这时候拿到Article的时候就会带一个版本号,比如拿到的版本是1,然后你对这个Article一通操作,操作完之后要插入到数据库了。发现哎呀,怎么数据库里的Article版本是2,和我手里的版本不一样啊,说明我手里的Article不是最新的了,那么就不能放到数据库了。这样就避免了并发时数据冲突的问题。所以我们现在给article表加一个字段versionarticle 文章表字段类型备注versionINT DEFAULT 0版本号然后对应的实体类也增加version字段@Data@Entitypublic class Article { … private Long version;}接着在ArticleRepository增加更新的方法,注意这里是更新方法,和悲观锁时增加查询方法不同。public interface ArticleRepository extends CrudRepository<Article, Long> { @Modifying @Query(value = “update article set comment_count = :commentCount, version = version + 1 where id = :id and version = :version”, nativeQuery = true) int updateArticleWithVersion(Long id, Long commentCount, Long version);}可以看到update的where有一个判断version的条件,并且会set version = version + 1。这就保证了只有当数据库里的版本号和要更新的实体类的版本号相同的时候才会更新数据。接着在CommentService里稍微修改一下代码。// CommentServicepublic void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); … int count = articleRepository.updateArticleWithVersion(article.getId(), article.getCommentCount() + 1, article.getVersion()); if (count == 0) { throw new RuntimeException(“服务器繁忙,更新数据失败”); } // articleRepository.save(article);}首先对于Article的查询方法只需要普通的findById()方法就行不用上任何锁。然后更新Article的时候改用新加的updateArticleWithVersion()方法。可以看到这个方法有个返回值,这个返回值代表更新了的数据库行数,如果值为0的时候表示没有符合条件可以更新的行。这之后就可以由我们自己决定怎么处理了,这里是直接回滚,spring就会帮我们回滚之前的数据操作,把这次的所有操作都取消以保证数据的一致性。现在再用测试用例测一下select * from articleselect count() comment_count from comment现在看到Article里的comment_count和Comment的数量都不是100了,但是这两个的值必定是一样的了。因为刚才我们处理的时候假如Article表的数据发生了冲突,那么就不会更新到数据库里,这时抛出异常使其事务回滚,这样就能保证没有更新Article的时候Comment也不会插入,就解决了数据不统一的问题。这种直接回滚的处理方式用户体验比较差,通常来说如果判断Article更新条数为0时,会尝试重新从数据库里查询信息并重新修改,再次尝试更新数据,如果不行就再查询,直到能够更新为止。当然也不会是无线的循环这样的操作,会设置一个上线,比如循环3次查询修改更新都不行,这时候才会抛出异常。利用JPA实现版本现解决并发问题JPA对悲观锁有实现方式,乐观锁自然也是有的,现在就用JPA自带的方法实现乐观锁。首先在Article实体类的version字段上加上@Version注解,我们进注解看一下源码的注释,可以看到有部分写到:The following types are supported for version properties: int, Integer, short, Short, long, Long, java.sql.Timestamp.注释里面说版本号的类型支持int, short, long三种基本数据类型和他们的包装类以及Timestamp,我们现在用的是Long类型。@Data@Entitypublic class Article { … @Version private Long version;}接着只需要在CommentService里的评论流程修改回我们最开头的“会触发并发问题”的业务代码就行了。说明JPA的这种乐观锁实现方式是非侵入式的。// CommentServicepublic void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); … article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article);}和前面同样的,用测试用例测试一下能否防止并发问题的出现。select * from articleselect count() comment_count from comment同样的Article里的comment_count和Comment的数量也不是100,但是这两个数值肯定是一样的。看一下IDEA的控制台会发现系统抛出了ObjectOptimisticLockingFailureException的异常。这和刚才我们自己实现乐观锁类似,如果没有成功更新数据则抛出异常回滚保证数据的一致性。如果想要实现重试流程可以捕获ObjectOptimisticLockingFailureException这个异常,通常会利用AOP+自定义注解来实现一个全局通用的重试机制,这里就是要根据具体的业务情况来拓展了,想要了解的可以自行搜索一下方案。悲观锁和乐观锁比较悲观锁适合写多读少的场景。因为在使用的时候该线程会独占这个资源,在本文的例子来说就是某个id的文章,如果有大量的评论操作的时候,就适合用悲观锁,否则用户只是浏览文章而没什么评论的话,用悲观锁就会经常加锁,增加了加锁解锁的资源消耗。乐观锁适合写少读多的场景。由于乐观锁在发生冲突的时候会回滚或者重试,如果写的请求量很大的话,就经常发生冲突,经常的回滚和重试,这样对系统资源消耗也是非常大。所以悲观锁和乐观锁没有绝对的好坏,必须结合具体的业务情况来决定使用哪一种方式。另外在阿里巴巴开发手册里也有提到:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。阿里巴巴建议以冲突概率20%这个数值作为分界线来决定使用乐观锁和悲观锁,虽然说这个数值不是绝对的,但是作为阿里巴巴各个大佬总结出来的也是一个很好的参考。 ...

December 18, 2018 · 3 min · jiezi

Java并发编程——线程基础查漏补缺

Thread使用Java的同学对Thread应该不陌生了,线程的创建和启动等这里就不讲了,这篇主要讲几个容易被忽视的方法,以及线程状态的迁移,最后会讲如何优雅的关闭线程。wait/notify/notifyAll首先我们要明白这三个方法是定义在Object类中,他们起到的作用就是允许线程就资源的锁定状态进行通信。这里所说的资源一般就是指的我们常说的共享对象了,也就是说针对共享对象的锁定状态可以通过wait/notify/notifyAll来进行通信。我们先看下如何使用的,并对相应原理进行展开。waitwait方法告诉调用线程放弃锁定并进入休眠状态,直到其他某个线程进入同一个监视器(monitor)并调用notify方法。wait方法在等待之前释放锁,并在wait方法返回之前重新获取锁。wait方法实际上和同步锁紧密集成,补充同步机制无法直接实现的功能。需要注意到wait方法在jdk源码中是final并且是native的本地方法,我们无法去覆盖此方法。调用wait一般的方式如下:synchronized(lockObject) { while(!condition) { lockObject.wait(); } //…;}join

December 17, 2018 · 1 min · jiezi

一次生产 CPU 100% 排查优化实践

前言到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。还真是想什么来什么,前些天还故意把某些服务器的负载提高(没错,老板让我写个 BUG!),不过还好是不同的环境互相没有影响。定位问题拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 ps 命令拿到了应用的 PID。接着使用 ps -Hp pid 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。果然某些线程的 CPU 使用率非常高。为了方便定位问题我立马使用 jstack pid > pid.log 将线程栈 dump 到日志文件中。我在上面 100% 的线程中随机选了一个 pid=194283 转换为 16 进制(2f6eb)后在线程快照中查询:因为线程快照中线程 ID 都是16进制存放。发现这是 Disruptor 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 OOM:强如 Disruptor 也发生内存溢出?没想到又来一出。为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。http://fastthread.io/其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。也就是说都是 Disruptor 队列的堆栈,同时都在执行 java.lang.Thread.yield 函数。众所周知 yield 函数会让当前线程让出 CPU 资源,再让其他线程来竞争。根据刚才的线程快照发现处于 RUNNABLE 状态并且都在执行 yield 函数的线程大概有 30几个。因此初步判断为大量线程执行 yield 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 Disruptor 有关。解决问题而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 Disruptor 队列来解耦。假设现在有 7 个业务类型,那就等于是创建 2*7=14 个 Disruptor 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。同时发现配置的消费等待策略为 YieldingWaitStrategy 这种等待策略确实会执行 yield 来让出 CPU。代码如下:初步看来和这个等待策略有很大的关系。本地模拟为了验证,我在本地创建了 15 个 Disruptor 队列同时结合监控观察 CPU 的使用情况。创建了 15 个 Disruptor 队列,同时每个队列都用线程池来往 Disruptor队列 里面发送 100W 条数据。消费程序仅仅只是打印一下。跑了一段时间发现 CPU 使用率确实很高。同时 dump 线程发现和生产的现象也是一致的:消费线程都处于 RUNNABLE 状态,同时都在执行 yield。通过查询 Disruptor 官方文档发现:YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用自旋 + yield的方式来提高性能。当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。同时查阅到其他的等待策略 BlockingWaitStrategy (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。于是在和之前同样的条件下将等待策略换为 BlockingWaitStrategy。和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。优化解决看样子将等待策略换为 BlockingWaitStrategy 可以减缓 CPU 的使用,但留意到官方对 YieldingWaitStrategy 的描述里谈道:当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 Disruptor 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 YieldingWaitStrategy)。跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。总结所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 Disruptor 队列。由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。所以我们的调整方式如下:为了快速缓解这个问题,先将等待策略换为 BlockingWaitStrategy,可以有效降低 CPU 的使用率(业务上也还能接受)。第二步就需要将应用拆分(上文模拟的一个 Disruptor 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。本文的演示代码已上传至 GitHub:https://github.com/crossoverJie/JCSprout你的点赞与分享是对我最大的支持 ...

December 17, 2018 · 1 min · jiezi

Golang并发模型:轻松入门select

之前的文章都提到过,Golang的并发模型都来自生活,select也不例外。举个例子:我们都知道一句话,“吃饭睡觉打豆豆”,这一句话里包含了3件事:妈妈喊你吃饭,你去吃饭。时间到了,要睡觉。没事做,打豆豆。在Golang里,select就是干这个事的:到吃饭了去吃饭,该睡觉了就睡觉,没事干就打豆豆。结束发散,我们看下select的功能,以及它能做啥。select功能在多个通道上进行读或写操作,让函数可以处理多个事情,但1次只处理1个。以下特性也都必须熟记于心:每次执行select,都会只执行其中1个case或者执行default语句。当没有case或者default可以执行时,select则阻塞,等待直到有1个case可以执行。当有多个case可以执行时,则随机选择1个case执行。case后面跟的必须是读或者写通道的操作,否则编译出错。select长下面这个样子,由select和case组成,default不是必须的,如果没其他事可做,可以省略default。func main() { readCh := make(chan int, 1) writeCh := make(chan int, 1) y := 1 select { case x := <-readCh: fmt.Printf(“Read %d\n”, x) case writeCh <- y: fmt.Printf(“Write %d\n”, y) default: fmt.Println(“Do what you want”) }}我们创建了readCh和writeCh2个通道:readCh中没有数据,所以case x := <-readCh读不到数据,所以这个case不能执行。writeCh是带缓冲区的通道,它里面是空的,可以写入1个数据,所以case writeCh <- y可以执行。有case可以执行,所以default不会执行。这个测试的结果是$ go run example.goWrite 1用打豆豆实践select来,我们看看select怎么实现打豆豆:eat()函数会启动1个协程,该协程先睡几秒,事件不定,然后喊你吃饭,main()函数中的sleep是个定时器,每3秒喊你吃1次饭,select则处理3种情况:从eatCh中读到数据,代表有人喊我吃饭,我要吃饭了。从sleep.C中读到数据,代表闹钟时间到了,我要睡觉。default是,没人喊我吃饭,也不到时间睡觉,我就打豆豆。import ( “fmt” “time” “math/rand”)func eat() chan string { out := make(chan string) go func (){ rand.Seed(time.Now().UnixNano()) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) out <- “Mom call you eating” close(out) }() return out}func main() { eatCh := eat() sleep := time.NewTimer(time.Second * 3) select { case s := <-eatCh: fmt.Println(s) case <- sleep.C: fmt.Println(“Time to sleep”) default: fmt.Println(“Beat DouDou”) }}由于前2个case都要等待一会,所以都不能执行,所以执行default,运行结果一直是打豆豆:$ go run x.goBeat DouDou现在我们不打豆豆了,你把default和下面的打印注释掉,多运行几次,有时候会吃饭,有时候会睡觉,比如这样:$ go run x.goMom call you eating$ go run x.goTime to sleep$ go run x.goTime to sleepselect很简单但功能很强大,它让golang的并发功能变的更强大。这篇文章写的啰嗦了点,重点是为下一篇文章做铺垫,下一篇我们将介绍下select的高级用法。select的应用场景很多,让我总结一下,放在下一篇文章中吧。并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 12, 2018 · 1 min · jiezi

通俗易懂,JDK 并发容器总结

该文已加入开源项目:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识的文档类项目,Star 数接近 14 k)。地址:https://github.com/Snailclimb…一 JDK 提供的并发容器总结实战Java高并发程序设计》为我们总结了下面几种大家可能会在高并发程序设计中经常遇到和使用的 JDK 为我们提供的并发容器。先带大家概览一下,下面会一一介绍到。JDK提供的这些容器大部分在 java.util.concurrent 包中。ConcurrentHashMap: 线程安全的HashMapCopyOnWriteArrayList: 线程安全的List,在读多写少的场合性能非常好,远远好于Vector.ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。BlockingQueue: 这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。二 ConcurrentHashMap我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。关于 ConcurrentHashMap 相关问题,我在 《这几道Java集合框架面试题几乎必问》 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题:ConcurrentHashMap 和 Hashtable 的区别ConcurrentHashMap线程安全的具体实现方式/底层具体实现三 CopyOnWriteArrayList3.1 CopyOnWriteArrayList 简介public class CopyOnWriteArrayList<E>extends Objectimplements List<E>, RandomAccess, Cloneable, Serializable在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问List的内部数据,毕竟读取操作是安全的。这和我们之前在多线程章节讲过 ReentrantReadWriteLock 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK中提供了 CopyOnWriteArravList 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArravList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。那它是怎么做的呢?3.2 CopyOnWriteArravList 是如何做到的?CopyOnWriteArravList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。从 CopyOnWriteArravList 的名字就能看出CopyOnWriteArravList 是满足CopyOnWrite 的ArrayList,所谓CopyOnWrite 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。3.3 CopyOnWriteArravList 读取和写入源码简单分析3.3.1 CopyOnWriteArravList 读取操作的实现读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 /** The array, accessed only via getArray/setArray. / private transient volatile Object[] array; public E get(int index) { return get(getArray(), index); } @SuppressWarnings(“unchecked”) private E get(Object[] a, int index) { return (E) a[index]; } final Object[] getArray() { return array; }3.3.2 CopyOnWriteArravList 写入操作的实现CopyOnWriteArravList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 /* * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) / public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock();//加锁 try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock();//释放锁 } }四 ConcurrentLinkedQueueJava提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。从名字可以看出,ConcurrentLinkedQueue这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的ConcurrentLinkedQueue来替代。五 BlockingQueue5.1 BlockingQueue 简单介绍上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:下面主要介绍一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。5.2 ArrayBlockingQueueArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。ArrayBlockingQueue一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);5.3 LinkedBlockingQueueLinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足FIFO的特性,与ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于Integer.MAX_VALUE。相关构造方法: /* 某种意义上的无界队列 * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. / public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } / *有界队列 * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity} is not greater * than zero */ public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }5.4 PriorityBlockingQueuePriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。PriorityBlockingQueue 并发控制采用的是 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。推荐文章:《解读 Java 并发队列 BlockingQueue》https://javadoop.com/post/java-concurrent-queue六 ConcurrentSkipListMap下面这部分内容参考了极客时间专栏《数据结构与算法之美》以及《实战Java高并发程序设计》。为了引出ConcurrentSkipListMap,先带着大家简单理解一下跳表。对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。跳表的本质是同时维护了多个链表,并且链表是分层的,最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的了集。跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素18。查找18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。从上面很容易看出,跳表是一种利用空间换时间的算法。使用跳表实现Map 和使用哈希算法实现Map的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是ConcurrentSkipListMap。七 参考《实战Java高并发程序设计》https://javadoop.com/post/jav…https://juejin.im/post/5aeebd...ThoughtWorks准入职Java工程师。专注Java知识分享!开源 Java 学习指南——JavaGuide(12k+ Star)的作者。公众号多篇文章被各大技术社区转载。公众号后台回复关键字“1”可以领取一份我精选的Java资源哦! ...

December 10, 2018 · 2 min · jiezi

Golang并发模型:轻松入门流水线FAN模式

前一篇文章《Golang并发模型:轻松入门流水线模型》,介绍了流水线模型的概念,这篇文章是流水线模型进阶,介绍FAN-IN和FAN-OUT,FAN模式可以让我们的流水线模型更好的利用Golang并发,提高软件性能。但FAN模式不一定是万能,不见得能提高程序的性能,甚至还不如普通的流水线。我们先介绍下FAN模式,再看看它怎么提升性能的,它是不是万能的。这篇文章内容略多,本来打算分几次写的,但不如一次读完爽,所以干脆还是放一篇文章了,要是时间不充足,利用好碎片时间,可以每次看1个标题的内容。FAN-IN和FAN-OUT模式Golang的并发模式灵感来自现实世界,这些模式是通用的,毫无例外,FAN模式也是对当前世界的模仿。以汽车组装为例,汽车生产线上有个阶段是给小汽车装4个轮子,可以把这个阶段任务交给4个人同时去做,这4个人把轮子都装完后,再把汽车移动到生产线下一个阶段。这个过程中,就有任务的分发,和任务结果的收集。其中任务分发是FAN-OUT,任务收集是FAN-IN。FAN-OUT模式:多个goroutine从同一个通道读取数据,直到该通道关闭。OUT是一种张开的模式,所以又被称为扇出,可以用来分发任务。FAN-IN模式:1个goroutine从多个通道读取数据,直到这些通道关闭。IN是一种收敛的模式,所以又被称为扇入,用来收集处理的结果。FAN-IN和FAN-OUT实践我们这次试用FAN-OUT和FAN-IN,解决《Golang并发模型:轻松入门流水线模型》中提到的问题:计算一个整数切片中元素的平方值并把它打印出来。producer()保持不变,负责生产数据。squre()也不变,负责计算平方值。修改main(),启动3个square,这3个squre从producer生成的通道读数据,这是FAN-OUT。增加merge(),入参是3个square各自写数据的通道,给这3个通道分别启动1个协程,把数据写入到自己创建的通道,并返回该通道,这是FAN-IN。FAN模式流水线示例:package mainimport ( “fmt” “sync”)func producer(nums …int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func merge(cs …<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup collect := func(in <-chan int) { defer wg.Done() for n := range in { out <- n } } wg.Add(len(cs)) // FAN-IN for _, c := range cs { go collect(c) } // 错误方式:直接等待是bug,死锁,因为merge写了out,main却没有读 // wg.Wait() // close(out) // 正确方式 go func() { wg.Wait() close(out) }() return out}func main() { in := producer(1, 2, 3, 4) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for ret := range merge(c1, c2, c3) { fmt.Printf("%3d “, ret) } fmt.Println()}3个squre协程并发运行,结果顺序是无法确定的,所以你得到的结果,不一定与下面的相同。➜ awesome git:(master) ✗ go run hi.go 1 4 16 9 FAN模式真能提升性能吗?相信你心里已经有了答案,可以的。我们还是使用老问题,对比一下简单的流水线和FAN模式的流水线,修改下代码,增加程序的执行时间:produer()使用参数生成指定数量的数据。square()增加阻塞操作,睡眠1s,模拟阶段的运行时间。main()关闭对结果数据的打印,降低结果处理时的IO对FAN模式的对比。普通流水线:// hi_simple.gopackage mainimport ( “fmt”)func producer(n int) <-chan int { out := make(chan int) go func() { defer close(out) for i := 0; i < n; i++ { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n // simulate time.Sleep(time.Second) } }() return out}func main() { in := producer(10) ch := square(in) // consumer for _ = range ch { }}使用FAN模式的流水线:// hi_fan.gopackage mainimport ( “sync” “time”)func producer(n int) <-chan int { out := make(chan int) go func() { defer close(out) for i := 0; i < n; i++ { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n // simulate time.Sleep(time.Second) } }() return out}func merge(cs …<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup collect := func(in <-chan int) { defer wg.Done() for n := range in { out <- n } } wg.Add(len(cs)) // FAN-IN for _, c := range cs { go collect(c) } // 错误方式:直接等待是bug,死锁,因为merge写了out,main却没有读 // wg.Wait() // close(out) // 正确方式 go func() { wg.Wait() close(out) }() return out}func main() { in := producer(10) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for _ = range merge(c1, c2, c3) { }}多次测试,每次结果近似,结果如下:FAN模式利用了7%的CPU,而普通流水线CPU只使用了3%,FAN模式能够更好的利用CPU,提供更好的并发,提高Golang程序的并发性能。FAN模式耗时10s,普通流水线耗时4s。在协程比较费时时,FAN模式可以减少程序运行时间,同样的时间,可以处理更多的数据。➜ awesome git:(master) ✗ time go run hi_simple.gogo run hi_simple.go 0.17s user 0.18s system 3% cpu 10.389 total➜ awesome git:(master) ✗ ➜ awesome git:(master) ✗ time go run hi_fan.gogo run hi_fan.go 0.17s user 0.16s system 7% cpu 4.288 total也可以使用Benchmark进行测试,看2个类型的执行时间,结论相同。为了节约篇幅,这里不再介绍,方法和结果贴在Gist了,想看的朋友瞄一眼,或自己动手搞搞。FAN模式一定能提升性能吗?FAN模式可以提高并发的性能,那我们是不是可以都使用FAN模式?不行的,因为FAN模式不一定能提升性能。依然使用之前的问题,再次修改下代码,其他不变:squre()去掉耗时。main()增加producer()的入参,让producer生产10,000,000个数据。简单版流水线修改代码:// hi_simple.gofunc square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func main() { in := producer(10000000) ch := square(in) // consumer for _ = range ch { }}FAN模式流水线修改代码:// hi_fan.gopackage mainimport ( “sync”)func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func main() { in := producer(10000000) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for _ = range merge(c1, c2, c3) { }}结果,可以跑多次,结果近似:➜ awesome git:(master) ✗ time go run hi_simple.go go run hi_simple.go 9.96s user 5.93s system 168% cpu 9.424 total➜ awesome git:(master) ✗ time go run hi_fan.go go run hi_fan.go 23.35s user 11.51s system 297% cpu 11.737 total从这个结果,我们能看到2点。FAN模式可以提高CPU利用率。FAN模式不一定能提升效率,降低程序运行时间。优化FAN模式既然FAN模式不一定能提高性能,如何优化?不同的场景优化不同,要依具体的情况,解决程序的瓶颈。我们当前程序的瓶颈在FAN-IN,squre函数很快就完成,merge函数它把3个数据写入到1个通道的时候出现了瓶颈,适当使用带缓冲通道可以提高程序性能,再修改下代码merge()中的out修改为:out := make(chan int, 100)结果:➜ awesome git:(master) ✗ time go run hi_fan_buffered.go go run hi_fan_buffered.go 19.85s user 8.19s system 323% cpu 8.658 total使用带缓存通道后,程序的性能有了较大提升,CPU利用率提高到323%,提升了8%,运行时间从11.7降低到8.6,降低了26%。FAN模式的特点很简单,相信你已经掌握了,如果记不清了看这里,本文所有代码在该Github仓库。FAN模式很有意思,并且能提高Golang并发的性能,如果想以后运用自如,用到自己的项目中去,还是要写写自己的Demo,快去实践一把。下一篇,写流水线中协程的“优雅退出”,欢迎关注。如果这篇文章对你有帮助,请点个赞/喜欢,让我知道我的写作是有价值的,感谢。 ...

November 28, 2018 · 3 min · jiezi

Golang并发模型:轻松入门流水线模型

Golang作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,它有多种并发模型,通过流水线模型系列文章,你会更好的使用Golang并发特性,提高你的程序性能。这篇文章主要介绍流水线模型的流水线概念,后面文章介绍流水线模型的FAN-IN和FAN-OUT,最后介绍下如何合理的关闭流水线的协程。Golang的并发核心思路Golang并发核心思路是关注数据流动。数据流动的过程交给channel,数据处理的每个环节都交给goroutine,把这些流程画起来,有始有终形成一条线,那就能构成流水线模型。但我们先从简单的入手。从一个简单的流水线入手流水线并不是什么新奇的概念,它能极大的提高生产效率,在当代社会流水线非常普遍,我们用的几乎任何产品(手机、电脑、汽车、水杯),都是从流水线上生产出来的。以汽车为例,整个汽车流水线要经过几百个组装点,而在某个组装点只组装固定的零部件,然后传递给下一个组装点,最终一台完整的汽车从流水线上生产出来。Golang的并发模型灵感其实都来自我们生活,对软件而言,高的生产效率就是高的性能。在Golang中,流水线由多个阶段组成,每个阶段之间通过channel连接,每个节点可以由多个同时运行的goroutine组成。从最简单的流水线入手。下图的流水线由3个阶段组成,分别是A、B、C,A和B之间是通道aCh,B和C之间是通道bCh,A生成数据传递给B,B生成数据传递给C。流水线中,第一个阶段的协程是生产者,它们只生产数据。最后一个阶段的协程是消费者,它们只消费数据。下图中A是生成者,C是消费者,而B只是中间过程的处理者。举个例子,设计一个程序:计算一个整数切片中元素的平方值并把它打印出来。非并发的方式是使用for遍历整个切片,然后计算平方,打印结果。我们使用流水线模型实现这个简单的功能,从流水线的角度,可以分为3个阶段:遍历切片,这是生产者。计算平方值。打印结果,这是消费者。下面这段代码:producer()负责生产数据,它会把数据写入通道,并把它写数据的通道返回。square()负责从某个通道读数字,然后计算平方,将结果写入通道,并把它的输出通道返回。main()负责启动producer和square,并且还是消费者,读取suqre的结果,并打印出来。package mainimport ( “fmt”)func producer(nums …int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func main() { in := producer(1, 2, 3, 4) ch := square(in) // consumer for ret := range ch { fmt.Printf("%3d", ret) } fmt.Println()}结果:➜ awesome git:(master) ✗ go run hi.go 1 4 9 16这是一种原始的流水线模型,这种原始能让我们掌握流水线的思路。流水线的特点每个阶段把数据通过channel传递给下一个阶段。每个阶段要创建1个goroutine和1个通道,这个goroutine向里面写数据,函数要返回这个通道。有1个函数来组织流水线,我们例子中是main函数。如果你没了解过流水线,建议自己把以上的程序写一遍,如果遇到问题解决了,那才真正掌握了流水线模型的思路。下一篇,我将介绍流水线模型的FAN-IN、FAN-OUT,欢迎关注。如果这篇文章对你有帮助,请点个赞/喜欢,让我知道我的写作是有价值的,感谢。 ...

November 26, 2018 · 1 min · jiezi

一次 HashSet 所引起的并发问题

背景上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。心中暗道不好,因为监控系统从来不会告诉我应用完美无 bug,其实系统挺猥琐。打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。由于这个应用出问题非常影响用户体验;于是立马让运维保留现场 dump 线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。分析首先了解下这个应用大概是做什么的。简单来说就是从 MQ 中取出数据然后丢到后面的业务线程池中做具体的业务处理。而报警的队列正好就是这个线程池的队列。跟踪代码发现构建线程池的方式如下:ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());; put(poolName,executor);采用的是默认的 LinkedBlockingQueue 并没有指定大小(这也是个坑),于是这个队列的默认大小为 Integer.MAX_VALUE。由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。内存分析先利用 MAT 分析了内存,的到了如下报告。其中有两个比较大的对象,一个就是之前线程池存放任务的 LinkedBlockingQueue,还有一个则是 HashSet。当然其中队列占用了大量的内存,所以优先查看,HashSet 一会儿再看。由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。线程分析再来看看线程的分析,这里利用 fastthread.io 这个网站进行线程分析。因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。正好他们都处于 RUNNABLE 状态,同时堆栈如下:发现正好就是在处理上文提到的 HashSet,看这个堆栈是在查询 key 是否存在。通过查看 312 行的业务代码确实也是如此。这里的线程名字也是个坑,让我找了好久。定位分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。这里其实有一个前提忘记讲到:这个告警是凌晨三点发出的邮件,但并没有电话提醒之类的,所以大家都不知道。到了早上上班时才发现并立即 dump 了上面的证据。所有有一个很重要的事实:这几个业务线程在查询 HashSet 的时候运行了 6 7 个小时都没有返回。通过之前的监控曲线图也可以看出:操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。同时发现这个应用生产上运行的是 JDK1.7 ,所以我初步认为应该是在查询 key 的时候进入了 HashMap 的环形链表导致 CPU 高负载同时也进入了死循环。为了验证这个问题再次 review 了代码。整理之后的伪代码如下://线程池private ExecutorService executor;private Set<String> set = new hashSet();private void execute(){ while(true){ //从 MQ 中获取数据 String key = subMQ(); executor.excute(new Worker(key)) ; }}public class Worker extends Thread{ private String key ; public Worker(String key){ this.key = key; } @Override private void run(){ if(!set.contains(key)){ //数据库查询 if(queryDB(key)){ set.add(key); return; } } //达到某种条件时清空 set if(flag){ set = null ; } } }大致的流程如下:源源不断的从 MQ 中获取数据。将数据丢到业务线程池中。判断数据是否已经写入了 Set。没有则查询数据库。之后写入到 Set 中。这里有一个很明显的问题,那就是作为共享资源的 Set 并没有做任何的同步处理。这里会有多个线程并发的操作,由于 HashSet 其实本质上就是 HashMap,所以它肯定是线程不安全的,所以会出现两个问题:Set 中的数据在并发写入时被覆盖导致数据不准确。会在扩容的时候形成环形链表。第一个问题相对于第二个还能接受。通过上文的内存分析我们已经知道这个 set 中的数据已经不少了。同时由于初始化时并没有指定大小,仅仅只是默认值,所以在大量的并发写入时候会导致频繁的扩容,而在 1.7 的条件下又可能会形成环形链表。不巧的是代码中也有查询操作(contains()),观察上文的堆栈情况:发现是运行在 HashMap 的 465 行,来看看 1.7 中那里具体在做什么:已经很明显了。这里在遍历链表,同时由于形成了环形链表导致这个 e.next 永远不为空,所以这个循环也不会退出了。到这里其实已经找到问题了,但还有一个疑问是为什么线程池里的任务队列会越堆越多。我第一直觉是任务执行太慢导致的。仔细查看了代码发现只有一个地方可能会慢:也就是有一个数据库的查询。把这个 SQL 拿到生产环境执行发现确实不快,查看索引发现都有命中。但我一看表中的数据发现已经快有 7000W 的数据了。同时经过运维得知 MySQL 那台服务器的 IO 压力也比较大。所以这个原因也比较明显了:由于每消费一条数据都要去查询一次数据库,MySQL 本身压力就比较大,加上数据量也很高所以导致这个 IO 响应较慢,导致整个任务处理的就比较慢了。但还有一个原因也不能忽视;由于所有的业务线程在某个时间点都进入了死循环,根本没有执行完任务的机会,而后面的数据还在源源不断的进入,所以这个队列只会越堆越多!这其实是一个老应用了,可能会有人问为什么之前没出现问题。这是因为之前数据量都比较少,即使是并发写入也没有出现并发扩容形成环形链表的情况。这段时间业务量的暴增正好把这个隐藏的雷给揪出来了。所以还是得信墨菲他老人家的话。总结至此整个排查结束,而我们后续的调整措施大概如下:HashSet 不是线程安全的,换为 ConcurrentHashMap同时把 value 写死一样可以达到 set 的效果。根据我们后面的监控,初始化 ConcurrentHashMap 的大小尽量大一些,避免频繁的扩容。MySQL 中很多数据都已经不用了,进行冷热处理。尽量降低单表数据量。同时后期考虑分表。查数据那里调整为查缓存,提高查询效率。线程池的名称一定得取的有意义,不然是自己给自己增加难度。根据监控将线程池的队列大小调整为一个具体值,并且要有拒绝策略。升级到 JDK1.8。再一个是报警邮件酌情考虑为电话通知????。HashMap 的死循环问题在网上层出不穷,没想到还真被我遇到了。现在要满足这个条件还是挺少见的,比如 1.8 以下的 JDK 这一条可能大多数人就碰不到,正好又证实了一次墨菲定律。同时我会将文章更到这里,方便大家阅读和查询。https://crossoverjie.top/JCSprout/你的点赞与分享是对我最大的支持 ...

November 8, 2018 · 1 min · jiezi

并发编程面试必备:AQS 原理以及 AQS 同步组件总结

常见问题:AQS 原理?;CountDownLatch和CyclicBarrier了解吗,两者的区别是什么?用过Semaphore吗?本节思维导图:【强烈推荐!非广告!】阿里云双11褥羊毛活动:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.hf47liqn 差不多一折,不过仅限阿里云新人购买,不是新人的朋友自己找方法买哦!1 AQS 简单介绍AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。2 AQS 原理在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。2.1 AQS 原理概览AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。看个AQS(AbstractQueuedSynchronizer)原理图:AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。private volatile int state;//共享变量,使用volatile修饰保证线程可见性状态信息通过procted类型的getState,setState,compareAndSetState进行操作//返回同步状态的当前值protected final int getState() { return state;} // 设置同步状态的值protected final void setState(int newState) { state = newState;}//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}2.2 AQS 对资源的共享方式AQS定义两种资源共享方式Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:公平锁:按照线程在队列中的排队顺序,先到者先拿到锁非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。2.3 AQS底层使用了模板方法模式同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票buyTicket()->安检securityCheck()->乘坐某某工具回家ride()->到达目的地arrive()。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了ride()方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 ride()方法。AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。推荐两篇 AQS 原理和相关源码分析的文章:http://www.cnblogs.com/watery…https://www.cnblogs.com/cheng...3 Semaphore(信号量)-允许多个线程同时访问synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。示例代码如下:/** * * @author Snailclimb * @date 2018年9月30日 * @Description: 需要一次性拿一个许可的情况 /public class SemaphoreExample1 { // 请求的数量 private static final int threadCount = 550; public static void main(String[] args) throws InterruptedException { // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) ExecutorService threadPool = Executors.newFixedThreadPool(300); // 一次只能允许执行的线程数量。 final Semaphore semaphore = new Semaphore(20); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> {// Lambda 表达式的运用 try { semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 test(threadnum); semaphore.release();// 释放一个许可 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); System.out.println(“finish”); } public static void test(int threadnum) throws InterruptedException { Thread.sleep(1000);// 模拟请求的耗时操作 System.out.println(“threadnum:” + threadnum); Thread.sleep(1000);// 模拟请求的耗时操作 }}执行 acquire 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 Semaphore经常用于限制获取某种资源的线程数量。当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 test(threadnum); semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4除了 acquire方法之外,另一个比较常用的与之对应的方法是tryAcquire方法,该方法如果获取不到许可就立即返回false。Semaphore 有两种模式,公平模式和非公平模式。公平模式: 调用acquire的顺序就是获取许可证的顺序,遵循FIFO;非公平模式: 抢占式的。Semaphore 对应的两个构造方法如下: public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。 由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章:https://blog.csdn.net/qq_1943…4 CountDownLatch (倒计时器)CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。4.1 CountDownLatch 的两种典型用法①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1) ,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。4.2 CountDownLatch 的使用示例/* * * @author SnailClimb * @date 2018年10月1日 * @Description: CountDownLatch 使用方法示例 /public class CountDownLatchExample1 { // 请求的数量 private static final int threadCount = 550; public static void main(String[] args) throws InterruptedException { // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) ExecutorService threadPool = Executors.newFixedThreadPool(300); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> {// Lambda 表达式的运用 try { test(threadnum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { countDownLatch.countDown();// 表示一个请求已经被完成 } }); } countDownLatch.await(); threadPool.shutdown(); System.out.println(“finish”); } public static void test(int threadnum) throws InterruptedException { Thread.sleep(1000);// 模拟请求的耗时操作 System.out.println(“threadnum:” + threadnum); Thread.sleep(1000);// 模拟请求的耗时操作 }}上面的代码中,我们定义了请求的数量为550,当这550个请求被处理完成之后,才会执行System.out.println(“finish”);。4.3 CountDownLatch 的不足CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。5 CyclicBarrier(循环栅栏)CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。5.1 CyclicBarrier 的应用场景CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。5.2 CyclicBarrier 的使用示例示例1:/* * * @author Snailclimb * @date 2018年10月1日 * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法 /public class CyclicBarrierExample2 { // 请求的数量 private static final int threadCount = 550; // 需要同步的线程数量 private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); public static void main(String[] args) throws InterruptedException { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < threadCount; i++) { final int threadNum = i; Thread.sleep(1000); threadPool.execute(() -> { try { test(threadNum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); } public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { System.out.println(“threadnum:” + threadnum + “is ready”); try { cyclicBarrier.await(2000, TimeUnit.MILLISECONDS); } catch (Exception e) { System.out.println("—–CyclicBarrierException——"); } System.out.println(“threadnum:” + threadnum + “is finish”); }}运行结果,如下:threadnum:0is readythreadnum:1is readythreadnum:2is readythreadnum:3is readythreadnum:4is readythreadnum:4is finishthreadnum:0is finishthreadnum:1is finishthreadnum:2is finishthreadnum:3is finishthreadnum:5is readythreadnum:6is readythreadnum:7is readythreadnum:8is readythreadnum:9is readythreadnum:9is finishthreadnum:5is finishthreadnum:8is finishthreadnum:7is finishthreadnum:6is finish……可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, await方法之后的方法才被执行。 另外,CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。示例代码如下:/* * * @author SnailClimb * @date 2018年10月1日 * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable */public class CyclicBarrierExample3 { // 请求的数量 private static final int threadCount = 550; // 需要同步的线程数量 private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { System.out.println("——当线程数达到之后,优先执行——"); }); public static void main(String[] args) throws InterruptedException { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < threadCount; i++) { final int threadNum = i; Thread.sleep(1000); threadPool.execute(() -> { try { test(threadNum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); } threadPool.shutdown(); } public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { System.out.println(“threadnum:” + threadnum + “is ready”); cyclicBarrier.await(); System.out.println(“threadnum:” + threadnum + “is finish”); }}运行结果,如下:threadnum:0is readythreadnum:1is readythreadnum:2is readythreadnum:3is readythreadnum:4is ready——当线程数达到之后,优先执行——threadnum:4is finishthreadnum:0is finishthreadnum:2is finishthreadnum:1is finishthreadnum:3is finishthreadnum:5is readythreadnum:6is readythreadnum:7is readythreadnum:8is readythreadnum:9is ready——当线程数达到之后,优先执行——threadnum:9is finishthreadnum:5is finishthreadnum:6is finishthreadnum:8is finishthreadnum:7is finish……5.3 CyclicBarrier和CountDownLatch的区别CountDownLatch是计数器,只能使用一次,而CyclicBarrier的计数器提供reset功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从jdk作者设计的目的来看,javadoc是这么描述它们的:CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。CyclicBarrier和CountDownLatch的区别这部分内容参考了如下两篇文章:https://blog.csdn.net/u010185…https://blog.csdn.net/tolcf/a...6 ReentrantLock 和 ReentrantReadWriteLockReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。由于篇幅问题,关于 ReentrantLock 和 ReentrantReadWriteLock 详细内容可以查看我的这篇原创文章。ReentrantLock 和 ReentrantReadWriteLock ...

November 2, 2018 · 4 min · jiezi