分而合之,并行不悖。html
技术人首先应当拥有良好的技术素养。技术素养首先是一丝不苟地严谨探索与求知的能力和精神,其次是对一个事物可以构建自底向上的逻辑,具备宏观清晰的视野和对细节的把握。java
要设法阅读第一手的资料,论文、官方文档、源码、经典著做,与发明者交流;要努力追溯本质与本源。知识是相对客观的,不能浮夸和造假。redis
技术人还应当保持谦逊,要明白本身所知的永远是冰山一角,以冰山一角去评判另外一个冰山一角,只是五十步比百步。
算法
并发,就是在同一时间段内有多个任务同时进行着。这些任务或者互不影响互不干扰,或者共同协做来完成一个更大的任务。编程
好比我在作项目 A,修改工程 a ; 你在作项目 B, 修改工程 b 。咱们各自作完本身的项目后上线。我和你作的事情就是并发的。若是我和你修改同一个工程,就可能须要协调处理冲突。并发是一种高效的运做方式,但每每也要处理并发带来的冲突和协做。后端
世界自然是并发的。本文总结并发相关的知识和实践。缓存
总入口见:“互联网应用服务端的经常使用技术思想与机制纲要”
安全
计算机中实现并发的方式有:多核、多进程、多线程;共享内存模型。基本方法是分而治之、划分均衡任务、独立工做单元、隔离访问共享资源。能够将一个大任务划分为多个互相协做的子任务,将一个大数据集划分为多个小的子数据集,分别处理后合并起来完成整个任务。并发须要解决执行实体之间的资源共享和通讯机制。服务器
多核:有多个 CPU 核心,每一个 CPU 核心都拥有专享的寄存器、高速缓存。多个 CPU 核心能够分别处理不一样的指令和数据集。多核心之间的通讯机制是系统总线和共享内存。多核是并发的硬件基础。网络
多进程模型:进程是程序的一次执行实例,具备私有地址空间,由内核调度。进程有父子关系。进程间通讯方式:管道(无名管道和命名管道 FIFO)、消息队列、共享内存、套接字、信号机制。进程建立和切换的开销都比较大。多进程是多任务执行的上下文基础。
多线程模型:线程是运行在进程上下文中的共享同一个进程私有地址空间的执行单元,亦由内核调度。线程之间是对等的。线程通讯方式:消息队列、共享内存。线程的建立和切换开销比进程要小不少。多线程是多任务的调度基础。
在 Java 应用语境中,执行实体对应着线程。如下涉及到执行实体的时候,直接以线程代替。并发能有效利用多个线程同时工做,大幅提高性能。同时,也是有必定代价的:线程阻塞与上下文切换(5000-10000 CPU 时钟)、内存同步开销(使 CPU 缓存失效、禁止编译器优化等)。不良的并发设计,可能致使大量线程等待、阻塞、切换,反而不如串行的执行效率高。
如何去思考和分析并发问题呢? 并发的难点在于,(不一样线程里的)任务执行的不一样顺序会引起不一样的结果,而这些顺序都是有必定几率性存在的。
所以,并发的关键点在于如何在合理的程度上协调任务执行顺序产生预期结果,同时又不对任务的进展产生过大的干预。就像宏观调控之于市场经济。市场经济是很是有活力的经济形式,但听凭市场经济的自由发展,会有失衡的风险。此时,就须要必定的宏观调控来干预一下。而宏观调控也不能过分,不然会抑制市场经济的活力。
注意,是协调执行顺序而不是控制。实际上,执行顺序是难以控制的。大多数时候,能作的是对少数步骤执行施加一些影响,使执行顺序符合某些前后约束,从而可以产生预期结果。绝大多数的步骤执行,仍是任之天然进行。
资源依赖
要正确协调执行顺序,先得弄清楚要协调哪些任务,或者说,任务执行受什么影响:
好比,同一个订单的下单过程,两个线程去分别读写订单数据(假设都是读 DB 主库):
所以,任务执行受有限共享的资源及资源依赖影响。若是多个任务并发执行,首先要理清楚这些任务所依赖的资源以及资源之间的依赖。资源类型包括:变量、数据行(记录)、文件句柄、网络链接、端口等。若是两个任务没有资源依赖,则各自执行便可;若是有共享资源依赖,则须要在合适的时候自动调节彼此获取共享资源的顺序。
值得说起的是,有一种隐式的资源依赖。好比一个大的任务拆分为 A,B,C 三个任务,A 和 B 都执行完成后,才能执行 C。此时 C 的执行依赖于 A,B 的执行完成状态(也极可能依赖 A,B 的执行结果集)。 这种隐式的资源依赖,也称为任务协做。
逻辑时钟
如何判断并发执行结果是准确的呢?好比 x = 1 。任务 A 在 t 时刻读 x ,任务 B 在 t+1 时刻写 x =5, 任务 C 在 t+2 读 x,按理 A 读到是 1 ,C 读到是 5 。 但因为网络延时,可能 C 的读请求在 B 的写请求提交以前就到达了,所以 C 也可能读到 1。因为网络的不可靠及机器各自的时钟是有细微不一样步的,所以,执行读写 x 的服务器没法判断 B, C 请求的前后性。
须要有一个逻辑时钟,给任务进行顺序编号,根据任务编号以及读写的因果性,就能判断 C 读到 1 的结果是错误的了。
happen-before
定一些基本的准则是必要的。就像欧几里得几何首先定义了五条公理而后才开始推导同样。
happen-before 是可见性判断的基本准则:符合准则的两个操做,前面的操做必然先行于/可见于后面的操做。换句话说,就是关于并发的基本定理。若是定理都不成立,那么并发的肯定性结果就无从谈起了。 happen-before 的具体细则:
要正确协调任务的执行顺序,须要解决任务之间的协做与同步。任务之间的协做与同步方式主要有:快照机制、原子操做、指令屏障、锁机制、信号机制、消息/管道机制。
快照机制
生成某个时间点的历史版本的不可变的快照数据,以必定策略去生成新的快照;直接读快照而不是读最新数据。将数据与版本号绑定,根据版本号来读取对应的数据;更新时不会修改已有的快照,而是生成新的版本号和数据。快照机制能够用来回溯历史数据。Git 是运用快照机制的典范。
快照机制并无对任务的天然进展施加影响,只是记录了某个数据集的某个时刻的状态。应用能够根据须要去读取不一样时刻的状态,作进一步处理。快照机制通常用来提高并发读的吞吐量。
原子操做
将多个操做封装为一个不可分割的总体操做,其它操做不可能在这个总体操做之间插入更新相关变量。
实现原子操做有两种方式:
指令屏障
指令屏障是在普通指令中插入特殊指令,从而在读写指令的执行之间加以执行顺序的前后约束,控制某些指令必须在另外一些指令以前执行且执行结果可见,禁止 CPU 经过指令重排序来优化内存读写(有性能损失)。最经常使用的指令屏障是内存屏障 Memory Barrier。
锁机制
锁机制用于有限共享资源的保护性访问,每次只容许一个执行体来访问可得到的共享资源。
锁机制的基础是 P-V 原语和阻塞/唤醒机制:
信号机制
信号机制是发出特定的信号,让接受信号的任务作相应的处理。中断是信号机制的一种典型场景。中断由某个中断源发出一个信号给某个线程,当线程收到这个信号时,能够作一些特定的动做。
Java 线程有一个中断标志位。处于不一样状态时,线程对于中断有不一样的反应。处于 New 和 Terminated 时,无心义;处于 Running 和 Blocked 时,只是设置中断标志位,不会影响线程状态; 处于 Time Sleep 时,会抛出异常并清空中断标志位。Java 将中断的具体处理的权力交给了应用。
消息/管道
经过在两个任务之间传递消息或者创建管道,来串联起两个任务的顺序执行。消息机制经常使用于解耦服务,而管道经常使用于 Pipeline 流水线模式中。
从并发思路中能够推导出一些经常使用的同步模式,来确保并发访问的安全性。主要有:Immutable、Unshared Copies、Monitor Locks 、Memory Barrier、Protected Lock、CAS。
Immutable
不可变数据。典型的不可变数据有字符串、快照。ES 分片里的倒排索引就是不可变的。ES 会将不可变的倒排索引与更新的倒排索引进行查询合并,获得最终的查询结果。
Unshared Copies
每一个线程都有一份本身的拷贝,不共享,互不影响。ThreadLocal 便是应用 Unshared Copies 模式。
Monitor locks
Java synchronized 块应用 Monitor locks 模式,基于 object monitor 和 monitorenter/2 monitorexit 实现,由编译器和 JVM 共同协做实现。JVM 规范指明:每一个对象关联一个 object monitor ,当线程执行 monitorenter 时会去获取 monitor 的 ownership ,而执行 monitorexit 则会释放 monitor 的 ownership。第二个 monitorexit 是为了在异常退出时与 monitorenter 匹配。在 hotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的。其源码位于 hotSpot 虚拟机源码 ObjectMonitor.hpp 文件中。
synchronized 方法是基于方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 标识符实现。synchronized 是可重入的,同一线程连续屡次获取同一个锁,不须要每次都加锁,只需记录加锁次数。同步容器 SynchronizedList, SynchronizedMap 是基于 synchronized(mutex) { // target.operation(); } 实现的对应容器的简单并发版。
Memory Barrier
Memory Barrier 内存屏障。当插入内存屏障后,其后的指令不会当即放在 CPU 缓存里,而是和内存屏障一块儿放在 FIFO 队列里,待 CPU 缓存里的指令都执行完成后,从 FIFO 中取出内存屏障后的指令来执行。
内存屏障主要有两种:
两两组合,有四种:StoreStoreBarrier, StoreLoadBarrier, LoadStoreBarrier, LoadLoadBarrier。 XYBarrier 是指,在 XYBarrier 以前的全部 X 操做都必须在 XYBarrier 以后的任一 Y 操做以前执行完成。而且写操做对全部处理器可见。
volatile 关键字在写操做以后插入 StoreStore Barrier, 在读操做以前操做 LoadLoad Barrier。volatile 适合作单个简单状态标识符的更新、生命周期里的初始化或退出。volatile 是不加锁的。
Protected Lock
轻量级更灵活的锁。形式一般以下:
Lock lock = lock.lock(lockKey, time, timeUnit); try { // doBizLogic; } finally { lock.unlock(); }
CAS
Compare-And-Swap 。CAS(V,E,N) 操做便是“先将 V 与 E 比较是否相等,若是相等,则更新到指定值 N ,不然什么都不作”。CAS 是无锁的非阻塞的,没有线程切换开销,所以在并发程度不高的状况下性能更优。Java 并发包里的绝大多数同步工具都有 CAS 的影子。 Java CAS 操做是经过 Unsafe 类的 native 方法支持的。
CAS 操做的原子语义是经过底层硬件和指令来支持的。相关指令以下:
在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能。CPU 的原子操做在底层能够经过总线锁定和缓存锁定来实现。总线锁定是 CPU 在总线上输出一个 LOCK# 信号,阻塞其它处理器操做该共享变量的缓存的请求,独占内存;缓存锁定是经过 MESI 缓存一致性机制来保证操做的原子性。在以下状况下只能使用总线锁定:当操做的数据不能被缓存在处理器内部,或者操做的数据跨多个缓存行时,或者 CPU 不支持缓存锁定。
CAS 有两个问题:
理解了并发的模型、思路和模式以后,再来看并发工具如何实现。Java 并发包里的绝大多数同步工具都是基于 CAS 和 AQS 的。所以,深刻理解 CAS 和 AQS 是很是重要的。
AQS
实现 Java 同步工具的基本框架,也是整个 Java 并发包的核心基础类。AQS 实现了“根据某种许可获取的状况将线程入队/出队以及相应的线程阻塞/唤醒”的通用机制,而将什么时候入队/出队(是否可以得到许可)的控制权交给了库的使用者。AQS 支持按照中断(互斥)或者超时两种模式来获取/释放许可,协调线程执行顺序。
AQS 包含一个同步队列和一个条件队列。两个队列都是基于链表实现的。
链表节点包含以下成员:
如前所述,AQS 实现了通用的入队/出队以及相应的阻塞/唤醒机制,那么什么时候会入队/出队呢?这就是自定义方法的做用了。使用 AQS 开发同步工具,须要定义好 state 的同步语义,实现以下方法:tryAcquire/tryRelease,tryAcquireShared/tryReleaseShared,isHeldExclusively。
AtomicXXX
原子类,提供基本数据类型的原子化更新操做。经过 volatile variable + offset (字段的固定的内存地址偏移量) + Unsafe 来获取的状态字段的可见值,CAS 实现原子操做,适用于计数、安全引用更新等。可阅读 AtomicInteger 和 LongAdder 的实现。
ReentrantLock
Protected Lock 模式的一种实现。基于 CAS 和 AQS 实现,提供公平锁 FairSync 和非公平锁 NonfairSync。默认非公平锁。非公平锁吞吐量更高,公平锁倾向于访问授予等待时间最长的线程,吞吐量可能较低,适合防线程饥饿上波动小一点。
ReentrantLock 能够返回一个ConditionObject 对象,用做条件等待阻塞和唤醒。
ConcurrentHashMap
HashMap 的并发加锁版。要点以下:
ConcurrentHashMap 体现了一些提高并发性能的技巧:减小串行化部分的耗时、减小持锁逻辑耗时(下降锁粒度)、减小锁竞争程度(数据分段及分段锁)。使用多个细粒度锁交互时要注意防止死锁。
ThreadLocal
ThreadLocal 类里维护了一个哈希表 ThreadLocalMap[ThreadLocal, Value] ,每一个线程都持有一个对 ThreadLocalMap 的引用,在该线程里调用 ThreadLocal.setInitialValue 方法时被初始化。当调用某个 ThreadLocal 对象的 set 方法时,会先获取当前线程,而后将当前线程的 TheadLocal 对象及对应的值写入所持有的 ThreadLocalMap 中。ThreadLocal 对象的哈希码值是经过一个 AtomicInteger 每次自增 0x61c88647 获得的。0x61c88647 是斐波那契乘数,可保证哈希散列分布均匀一些。
ThreadLocal 在一个长流程中存储须要的 Context 。ThreadLocal 使用要注意的问题:
线程池
线程池是受控的可执行多任务的线程管理器。Java 线程池实现是 ThreadPoolExecutor。 线程池的主要组成部分以下:
线程池的实现要点以下:
要作到并发的准确与安全,须要很是当心地避免一些常见陷阱:
InnoDB锁
分布式锁
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1])
。 可使用 Redisson API 。大流量
并发大流量是引发应用不稳定甚至将应用击溃的常见杀手之一。应对并发大流量的措施:1. 缓存,减小对后端存储压力;2. 降级,暂时移除非核心链路;3. 限流; 4. 架构升级,作到动静分离、冷热分离、读写分离、服务器分离、服务分离、分库分表、负载均衡、NoSQL 技术、(多机房)冗余、容器化、上云。
不一致
因为任务顺序的不肯定性及脑力思考的局限性,加上大流量,在少量情形下,可能会触发程序的细微 BUG, 引发数据的不一致。
因为人力的有限性,对于高并发引发的不一致,最好能构建准实时的监控、对帐、补偿和对帐报表。
死锁
多个线程同时要获取多个类型的共享资源时,申请锁的顺序不当,可能致使死锁。
世界自然是并发的。并发既是一种高效的运做方式,亦是一种符合天然的设计。本文总结了并发的基础知识、思路、模式、工具、陷阱、应用、挑战。
PS: 当我回过头来看写下的这些知识时,发现大部分都是描述性的,只有少部分是原理性的。或者说,在某个层次上是原理性的知识,在更底层看来是描述性的知识。从描述性的知识中,应当提炼出原理与思想。
我忽然感到:不只要对整个模型和机制有宏观清晰的视野,也要能扎下去,研究第一手的论文和资料。如此,才能自下而上地融会贯通。而这样的探索,才是技术的精神本质。