一、问题1、什么是线程的交互方式2、如何区分线程的同步/异步阻塞/非阻塞3、什么是线程安全如何做到线程安全4、如何区分并发模型5、何谓响应式编程6、操作系统如何调度多线程二、关键词同步异步阻塞非阻塞并行并发临界区竞争条件指令重排锁amdahl,gustafson三、全文概要上一篇我们介绍分布式系统的知识体系《分布式架构知识体系》由于单机的性能上限原因我们才不得不发展分布式技术。那么话说回来如果单机的性能没能最大限度的榨取出来就盲目建设分布式系统那就有点本末倒置了。而且上一篇我们给的忠告是如果有可能的话不要用分布式意思是说如果单机性能满足的话就不要折腾复杂的分布式架构。如果说分布式架构是宏观上的性能扩展那么高并发则是微观上的性能调优这也是上一篇性能部分拆出来的大专题。本文将从线程的基础理论谈起逐步探究线程的内存模型线程的交互线程工具和并发模型的发展。扫除关于并发编程的诸多模糊概念从新构建并发编程的层次结构。四、基础理论4.1基本概念开始学习并发编程前我们需要熟悉一些理论概念。既然我们要研究的是并发编程那首先应该对并发这个概念有所理解才是而说到并发我们肯定要要讨论一些并行。并发一个处理器同时处理多个任务并行多个处理器或者是多核的处理器同时处理多个不同的任务然后我们需要再了解一下同步和异步的区别同步执行某个操作开始后就一直按部就班等着直到操作结束异步执行某个操作后立即离开后面有响应的话再来通知执行者接着我们再了解一个重要的概念临界区公共资源或者共享数据由于共享数据的出现必然会导致竞争所以我们需要再了解一下阻塞某个操作需要的共享资源被占用了只能等待称为阻塞非阻塞某个操作需要的共享资源被占用了不等待立即返回并携带错误信息回去期待重试如果两个操作都在等待某个共享资源而且都互不退让就会造成死锁死锁参考著名的哲学家吃饭问题饥饿饥饿的哲学家等不齐筷子吃饭活锁相互谦让而导致阻塞无法进入下一步操作跟死锁相反死锁是相互竞争而导致的阻塞4.2并发级别理想情况下我们希望所有线程都一起并行飞起来。但是CPU数量有限线程源源不断总得有个先来后到不同场景需要的并发需求也不一样比如秒杀系统我们需要很高的并发程度但是对于一些下载服务我们需要的是更快的响应并发反而是其次的。所以我们也定义了并发的级别来应对不同的需求场景。阻塞阻塞是指一个线程进入临界区后其它线程就必须在临界区外等待待进去的线程执行完任务离开临界区后其它线程才能再进去。无饥饿线程排队先来后到不管优先级大小先来先执行就不会产生饥饿等待资源也即公平锁相反非公平锁则是根据优先级来执行有可能排在前面的低优先级线程被后面的高优先级线程插队就形成饥饿无障碍共享资源不加锁每个线程都可以自有读写单监测到被其他线程修改过则回滚操作重试直到单独操作成功风险就是如果多个线程发现彼此修改了所有线程都需要回滚就会导致死循环的回滚中造成死锁无锁无锁是无障碍的加强版无锁级别保证至少有一个线程在有限操作步骤内成功退出不管是否修改成功这样保证了多个线程回滚不至于导致死循环无等待无等待是无锁的升级版并发编程的最高境界无锁只保证有线程能成功退出但存在低级别的线程一直处于饥饿状态无等待则要求所有线程必须在有限步骤内完成退出让低级别的线程有机会执行从而保证所有线程都能运行提高并发度。4.3量化模型首先多线程不意味着并发但并发肯定是多线程或者多进程。我们知道多线程存在的优势是能够更好地利用资源有更快的请求响应。但是我们也深知一旦进入多线程附带而来的是更高的编码复杂度线程设计不当反而会带来更高的切换成本和资源开销。但是总体上我们肯定知道利大于弊这不是废话吗不然谁还愿意去搞多线程并发程序但是如何衡量多线程带来的效率提升呢我们需要借助两个定律来衡量。AmdahlS1/(1-aa/n)其中a为并行计算部分所占比例n为并行处理结点个数。这样当1-a0时(即没有串行只有并行)最大加速比sn当a0时即只有串行没有并行最小加速比s1当n→∞时极限加速比s→ 1/1-a这也就是加速比的上限。Gustafson系统优化某部件所获得的系统性能的改善程度取决于该部件被使用的频率或所占总执行时间的比例。两面列举了这两个定律来衡量系统改善后提升效率的量化指标具体的应用我们在下文的线程调优会再详细介绍。五、内存模型宏观上分布式系统需要解决的首要问题是数据一致性同样微观上并发编程要解决的首要问题也是数据一致性。貌似我们搞了这么多年的斗争都是在攻关一致性这个世界性难题。既然并发编程要从微观开始那么我们肯定要对CPU和内存的工作机理有所了解尤其是数据在CPU和内存直接的传输机制。5.1整体原则探究内存模型之前我们要抛出三个概念:原子性在32位的系统中对于4个字节32位的Integer的操作对应的JVM指令集映射到汇编指令为一个原子操作所以对Integer类型的数据操作是原子性但是Long类型为8个字节64位32位系统要分为两条指令来操作所以不是原子操作。对于32位操作系统来说单次操作能处理的最长长度为32bit而long类型8字节64bit所以对long的读写都要两条指令才能完成即每次读写64bit中的32bit可见性线程修改变量对其他线程即时可见有序性串行指令顺序唯一并行线程直接指令可能出现不一致也即指令被重排。而指令重排也是有一定原则(摘自《深入理解Java虚拟机第12章》)程序次序规则一个线程内按照代码顺序书写在前面的操作先行发生于书写在后面的操作锁定规则一个unLock操作先行发生于后面对同一个锁的lock操作volatile变量规则对一个变量的写操作先行发生于后面对这个变量的读操作传递规则如果操作A先行发生于操作B而操作B又先行发生于操作C则可以得出操作A先行发生于操作C线程启动规则Thread对象的start()方法先行发生于此线程的每个一个动作线程中断规则对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终结规则线程中所有的操作都先行发生于线程的终止检测我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行对象终结规则一个对象的初始化完成先行发生于他的finalize()方法5.2逻辑内存我们谈的逻辑内存也即是JVM的内存格局。JVM将操作系统提供的物理内存和CPU缓存在逻辑分为堆栈方法区和程序计数器。在《从宏观微观角度浅析JVM虚拟机》 一文我们详细介绍了JVM的内存模型分布并发编程我们主要关注的是堆栈的分配因为线程都是寄生在栈里面的内存段把栈里面的方法逻辑读取到CPU进行运算。5.3物理内存而实际的物理内存包含了主存和CPU的各级缓存还有寄存器而为了计算效率CPU往往会就近从缓存里面读取数据。在并发的情况下就会造成多个线程之间对共享数据的错误使用。5.4内存映射由于可能发生对象的变量同时出现在主存和CPU缓存中就可能导致了如下问题线程修改的变量对外可见读写共享变量时出现竞争资源由于线程内的变量对栈外是不可见的但是成员变量等共享资源是竞争条件所有线程可见就会出现如下当一个线程从主存拿了一个变量1修改后变成2存放在CPU缓存还没来得及同步回主存时另外一个线程又直接从主存读取变量为1这样就出现了脏读。现在我们弄清楚了线程同步过程数据不一致的原因接下来要解决的目标就是如何避免这种情况的发生经过大量的探索和实践我们从概念上不断的革新比如并发模型的流水线化和无状态函数式化而且也提供了大量的实用工具。接下来我们从无到有先了解最简单的单个线程的一些特点弄清楚一个线程有多少能耐后才能深刻认识多个线程一起打交道会出现什么幺蛾子。六、线程单元6.1状态我们知道应用启动体现的就是静态指令加载进内存进而进入CPU运算操作系统在内存开辟了一段栈内存用来存放指令和变量值从而形成了进程。而其实我们的JVM也就是一个进程而线程是进程的最小单位也就是说进程是由很多个线程组成的。而由于进程的上下文关联的变量引用计数器等现场数据占用了打段的内存空间所以频繁切换进程需要整理一大段内存空间来保存未执行完的进程现场等下次轮到CPU时间片再恢复现场进行运算。这样既耗费时间又浪费空间所以我们才要研究多线程。毕竟由于线程干的活毕竟少工作现场数据毕竟少所以切换起来比较快而且暂用少量空间。而线程切换直接也需要遵守一定的法则不然到时候把工作现场破坏了就无法恢复工作了。线程状态我们先来研究线程的生命周期看看Thread类里面对线程状态的定义就知道public enum State { /** * Thread state for a thread which has not yet started. */ NEW,/** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE,/** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {link Object#wait() Object.wait}. */ BLOCKED,/** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * ul * li{link Object#wait() Object.wait} with no timeout/li * li{link #join() Thread.join} with no timeout/li * li{link LockSupport#park() LockSupport.park}/li * /ul * * pA thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called ttObject.wait()/tt * on an object is waiting for another thread to call * ttObject.notify()/tt or ttObject.notifyAll()/tt on * that object. A thread that has called ttThread.join()/tt * is waiting for a specified thread to terminate. */ WAITING,/** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * ul * li{link #sleep Thread.sleep}/li * li{link Object#wait(long) Object.wait} with timeout/li * li{link #join(long) Thread.join} with timeout/li * li{link LockSupport#parkNanos LockSupport.parkNanos}/li * li{link LockSupport#parkUntil LockSupport.parkUntil}/li * /ul */ TIMED_WAITING,/** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED;}生命周期线程的状态NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED。注释也解释得很清楚各个状态的作用而各个状态的转换也有一定的规则需要遵循的。6.2动作介绍完线程的状态和生命周期接下来了解线程具备哪些常用的操作。首先线程也是一个普通的对象Thread所有的线程都是Thread或者其子类的对象。那么这个内存对象被创建出来后就会放在JVM的堆内存空间当我们执行start()方法的时候对象的方法体在栈空间分配好对应的栈帧来往执行引擎输送指令也即是方法体翻译成JVM的指令集。线程操作新建线程new Thread()新建一个线程对象内存为线程在栈上分配好内存空间启动线程start()告诉系统系统准备就绪只要资源允许随时可以执行栈里面的指令执行线程run()分配了CPU等计算资源正在执行栈里面的指令集停止线程(过时)stop()把CPU和内存资源回收线程消亡由于太过粗暴已经被标记为过时线程中断interrupt()中断是对线程打上了中断标签可供run()里面的方法体接收中断信号至于线程要不要中断全靠业务逻辑设计而不是简单粗暴的把线程直接停掉isInterrupt()主要是run()方法体来判断当前线程是否被置为中断interrupted()静态方法也是用户判断线程是否被置为中断状态同时判断完将线程中断状态复位线程休眠sleep()静态方法线程休眠指定时间段此间让出CPU资源给其他线程但是线程依然持有对象锁其他线程无法进入同步块休眠完成后也未必立刻执行需要等到资源允许才能执行线程等待(对象方法)wait()是Object的方法也即是对象的内置方法在同步块中线程执行到该方法时也即让出了该对象的锁所以无法继续执行线程通知(对象方法)notify(),notifyAll()此时该对象持有一个或者多个线程的wait调用notify()随机的让一个线程恢复对象的锁调用notifyAll()则让所有线程恢复对象锁线程挂起(过时)suspend()线程挂起并没有释放资源而是只能等到resume()才能继续执行线程恢复(过时)resume()由于指令重排可能导致resume()先于suspend()执行导致线程永远挂起所以该方法被标为过时线程加入join()在一个线程调用另外一个线程的join()方法表明当前线程阻塞直到被调用线程执行结束再进行线程让步yield()暂停当前线程进而执行别的线程当前线程等待下一轮资源允许再进行防止该线程一直霸占资源而其他线程饿死线程等待park()基于线程对象的操作较对象锁更为精准线程恢复unpark(Thread thread)对应park()解锁为不可重入锁线程分组为了管理线程于是有了线程组的概念业务上把类似的线程放在一个ThreadGroup里面统一管理。线程组表示一组线程此外线程组还可以包括其他线程组。线程组形成一个树其中除了初始线程组以外的每个线程组都有一个父线程。线程被允许访问它自己的线程组信息但不能访问线程组的父线程组或任何其他线程组的信息。守护线程通常情况下线程运行到最后一条指令后则完成生命周期结束线程然后系统回收资源。或者单遇到异常或者return提前返回但是如果我们想让线程常驻内存的话比如一些监控类线程需要24小时值班的于是我们又创造了守护线程的概念。setDaemon()传入true则会把线程一直保持在内存里面除非JVM宕机否则不会退出。线程优先级线程优先级其实只是对线程打的一个标志但并不意味这高优先级的一定比低优先级的先执行具体还要看操作系统的资源调度情况。通常线程优先级为5边界为[1,10]。 /** * The minimum priority that a thread can have. */ public final static int MIN_PRIORITY 1;/** * The default priority that is assigned to a thread. */ public final static int NORM_PRIORITY 5;/** * The maximum priority that a thread can have. */ public final static int MAX_PRIORITY 10;本节介绍了线程单元的转态切换和常用的一些操作方法。如果只是单线程的话其他都没必要研究这些重头戏在于多线程直接的竞争配合操作下一节则重点介绍多个线程的交互需要关注哪些问题。七、线程交互其实上一节介绍的线程状态切换和线程操作都是为线程交互做准备的。不然如果只是单线程完全没必要搞什么通知恢复让步之类的操作了。7.1交互方式线程交互也就是线程直接的通信最直接的办法就是线程直接通信传值而间接方式则是通过共享变量来达到彼此的交互。等待释放对象锁允许其他线程进入同步块通知重新获取对象锁继续执行中断状态交互通知其他线程进入中断织入合并线程多个线程合并为一个7.2线程安全我们最关注的还是通过共享变量来达到交互的方式。线程如果都各自干活互不搭理的话自然相安无事但多数情况下线程直接需要打交道而且需要分享共享资源那么这个时候最核心的就是线程安全了。什么是线程安全当多个线程访问同一个对象时如果不用考虑这些线程在运行时环境下的调度和交替运行也不需要进行额外的同步或者在调用方进行任何其他的协调操作调用这个对象的行为都可以获取正确的结果那这个对象是线程安全的。摘自《深入Java虚拟机》如何保证线程安全我们最早接触线程安全可能是JDK提供的一些号称线程安全的容器比如Vetor较ArrayList是线程安全HashTable较HashMap是线程安全。其实线程安全类并不代表也不等同线程安全的程序而线程不安全的类同样可以完成线程安全的程序。我们关注的也就是写出线程安全的程序那么如何写出线程安全的代码呢下面列举了线程安全的主要设计技术无状态这个有点函数式编程的味道下文并发模式会介绍到总之就是线程只有入参和局部变量如果变量是引用的话确保变量的创建和调用生命周期都发生在线程栈内就可以确保线程安全。无共享状态完全要求线程无状态比较难实现必要的状态是无法避免的那么我们就必须维护不同线程之间的不同状态这可是个麻烦事。幸好我们有ThreadLocal这个神器该对象跟当前线程绑定而且只对当前线程可见完美解决了无共享状态的问题。不可变状态最后实在没办法避免状态共享在线程之间共享状态最怕的就是无法确保能维护好正确的读写顺序而且多线程确实也无法正确维护好这个共享变量。那么我们索性粗暴点把共享的状态定位不可变比如价格final修饰一下这样就达到安全状态共享。消息传递一个线程通常也不是所有步骤都需要共享状态而是部分环节才需要的那么我们把共享状态的代码拆开无共享状态的那部分自然不用关心而共享状态的小段代码则通过加入消息组件来传递状态。这个设计到并发模式的流水线编程模式下文并发模式会重点介绍。线程安全容器JUC里面提供大量的并发容器涉及到线程交互的时候使用安全容器可以避免大部分的错误而且大大降低了代码的复杂度。通过synchronized给方法加上内置锁来实现线程安全的类如VectorHashTable,StringBufferAtomicXXX如AtomicIntegerConcurrentXXX如ConcurrentHashMapBlockingQueue/BlockingDequeCopyOnWriteArrayList/CopyOnWriteArraySetThreadPoolExecutorsynchronized同步该关键字确保代码块同一时间只被一个线程执行在这个前提下再设计符合线程安全的逻辑其作用域为对象对象加锁进入同步代码块之前获取对象锁实例方法对象加锁执行实例方法前获取对象实例锁类方法类加锁执行类方法前获取类锁volatile约束volatile确保每次操作都能强制同步CPU缓存和主存直接的变量。而且在编译期间能阻止指令重排。读写并发情况下volatile也不能确保线程安全上文解析内存模型的时候有提到过。这节我们论述了编写线程安全程序的指导思想其中我们提到了JDK提供的JUC工具包下一节将重点介绍并发编程常用的趁手工具。八、线程工具前文我们介绍了内存理论和线程的一些特征大家都知道并发编程容易出错而且出了错还不好调试排查幸好JDK里面集成了大量实用的API工具我们能熟悉这些工具写起并发程序来也事半功倍。工具篇其实就是对锁的不断变种适应更多的开发场景提高性能提供更方便的工具从最粗暴的同步修饰符到灵活的可重入锁到宽松的条件接着到允许多个线程访问的信号量最后到读写分离锁。8.1同步控制由于大多数的并发场景都是需要访问到共享资源的为了保证线程安全我们不得已采用锁的技术来做同步控制这节我们介绍的是适用不同场景各种锁技术。ReentrantLock可重入互斥锁具有与使用synchronized的隐式监视器锁具有相同的行为和语义但具有更好扩展功能。ReentrantLock由最后成功锁定的线程拥有而且还未解锁。当锁未被其他线程占有时线程调用lock()将返回并且成功获取锁。如果当前线程已拥有锁则该方法将立即返回。这可以使用方法isHeldByCurrentThread()和getHoldCount()来检查。构造函数接受可选的fairness参数。当设置为true时在竞争条件下锁定有利于赋予等待时间最长线程的访问权限。否则锁将不保证特定的访问顺序。在多线程访问的情况使用公平锁比默认设置有着更低的吞吐量但是获得锁的时间比较小而且可以避免等待锁导致的饥饿。但是锁的公平性并不能保证线程调度的公平性。因此使用公平锁的许多线程中的一个可以连续多次获得它而其他活动线程没有进展并且当前没有持有锁。不定时的tryLock方法不遵循公平性设置。即使其他线程正在等待如果锁可用它也会成功。任意指定锁的起始位置中断响应锁申请等待限时tryLock()公平锁ConditionCondition从拥有监控方法wait,notify,notifyAll的Object对象中抽离出来成为独特的对象高效的让每个对象拥有更多的等待线程。和锁对比起来如果说用Lock代替synchronized那么Condition就是用来代替Object本身的监控方法。Condition实例跟Object本身的监控相似同样提供wait()方法让调用的线程暂时挂起让出资源知道其他线程通知该对象转态变化才可能继续执行。Condition实例来源于Lock实例通过Lock调用newCondition()即可。Condition较Object原生监控方法可以保证通知顺序。Semaphore锁和同步块同时只能允许单个线程访问共享资源这个明显有些单调部分场景其实可以允许多个线程访问这个时候信号量实例就派上用场了。信号量逻辑上维持了一组许可证 线程调用acquire()阻塞直到许可证可用后才能执行。执行release()意味着释放许可证实际上信号量并没有真正的许可证只是采用了计数功能来实现这个功能。ReadWriteLock顾名思义读写锁将读写分离细化了锁的粒度照顾到性能的优化。CountDownLatch这个锁有点“关门放狗”的意思尤其在我们压测的时候模拟实时并行请求该实例将线程积累到指定数量后调用countDown()方法让所有线程同时执行。CyclicBarrierCyclicBarrier是加强版的CountDownLatch上面讲的是一次性“关门放狗”而循环栅栏则是集齐了指定数量的线程在资源都允许的情况下同时执行然后下一批同样的操作周而复始。LockSupportLockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。因为park() 和 unpark()有许可的存在调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。8.2线程池线程池总览线程多起来的话就需要管理不然就会乱成一锅粥。我们知道线程在物理上对应的就是栈里面的一段内存存放着局部变量的空间和待执行指令集。如果每次执行都要从头初始化这段内存然后再交给CPU执行效率就有点低了。假如我们知道该段栈内存会被经常用到那我们就不要回收创建完就让它在栈里面呆着要用的时候取出来用完换回去是不是就省了初始化线程空间的时间这样是我们搞出线程池的初衷。其实线程池很简单就是搞了个池子放了一堆线程。既然我们搞线程池是为了提高效率那就要考虑线程池放多少个线程比较合适太多了或者太少了有什么问题怎么拒绝多余的请求除了异常怎么处理。首先我们来看跟线程池有关的一张类图。线程池归结起来就是这几个类的使用技巧了重点关注ThreadPoolExecutor和Executors即可。创建线程池万变不离其宗创建线程池的各种马甲方法最后都是调用到这方法里面包含核心线程数最大线程数线程工厂拒绝策略等参数。其中线程工厂则可以实现自定义创建线程的逻辑。public interface ThreadFactory { Thread newThread(Runnable r);}创建的核心构造方法ThreadPoolExecutor.java 1301 /** * Creates a new {code ThreadPoolExecutor} with the given initial * parameters. * * param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {code allowCoreThreadTimeOut} is set * param maximumPoolSize the maximum number of threads to allow in the * pool * param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * param unit the time unit for the {code keepAliveTime} argument * param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {code Runnable} * tasks submitted by the {code execute} method. * param threadFactory the factory to use when the executor * creates a new thread * param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * throws IllegalArgumentException if one of the following holds:br * {code corePoolSize 0}br * {code keepAliveTime 0}br * {code maximumPoolSize 0}br * {code maximumPoolSize corePoolSize} * throws NullPointerException if {code workQueue} * or {code threadFactory} or {code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)拒绝策略包含 /** 实际上并未真正丢弃任务但是线程池性能会下降 * A handler for rejected tasks that runs the rejected task * directly in the calling thread of the {code execute} method, * unless the executor has been shut down, in which case the task * is discarded. */ public static class CallerRunsPolicy implements RejectedExecutionHandler/** 粗暴停止抛异常 * A handler for rejected tasks that throws a * {code RejectedExecutionException}. */ public static class AbortPolicy implements RejectedExecutionHandler /** 悄无声息的丢弃拒绝的任务 * A handler for rejected tasks that silently discards the * rejected task. */ public static class DiscardPolicy implements RejectedExecutionHandler /** 丢弃最老的请求 * A handler for rejected tasks that discards the oldest unhandled * request and then retries {code execute}, unless the executor * is shut down, in which case the task is discarded. */ public static class DiscardOldestPolicy implements RejectedExecutionHandler 包括Executors.java中的创建线程池的方法具体实现也是通过ThreadPoolExecutor来创建的。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueueRunnable());}public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable());}public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);}public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable()));}调用线程池ThreadPoolExecutor.java 1342/** 同步执行线程出现异常打印堆栈信息 * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {code RejectedExecutionHandler}. * * param command the task to execute * throws RejectedExecutionException at discretion of * {code RejectedExecutionHandler}, if the task * cannot be accepted for execution * throws NullPointerException if {code command} is null */public void execute(Runnable command)/*** 异步提交线程任务出现异常无法同步追踪堆栈,本质上也是调用execute()方法*/public T FutureT submit(Runnable task, T result) { if (task null) throw new NullPointerException(); RunnableFutureT ftask newTaskFor(task, result); execute(ftask); return ftask;}线程池优化线程池已经是我们使用线程的一个优化成果了而线程池本身的优化其实就是根据实际业务选择好不同类型的线程池预估并发线程数量控制好线程池预留线程数最大线程数一般设为2N1最好N是CPU核数这些涉及CPU数量核数还有具体业务。另外我们还注意到ForkJoinPool继承了AbstractExecutorService这是在JDK7才加上去的目的就是提高任务派生出来更多任务的执行效率由上图的继承关系我们可以知道跟普通线程池最大的差异是执行的任务类型不同。public void execute(ForkJoinTask? task) { if (task null) throw new NullPointerException(); externalPush(task);}public void execute(Runnable task) { if (task null) throw new NullPointerException(); ForkJoinTask? job; if (task instanceof ForkJoinTask?) // avoid re-wrap job (ForkJoinTask?) task; else job new ForkJoinTask.RunnableExecuteAction(task); externalPush(job); }8.3并发容器其实我们日常开发大多数并发场景直接用JDK 提供的线程安全数据结构足矣下面列举了常用的列表集合等容器具体就不展开讲相信大家都用得很熟悉了。ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueueBlockingQueueConcurrentSkipListMapVectorHashTable...九、线程调优9.1性能指标回想一下当我们在谈性能优化的时候我们可能指的是数据库的读写次数也可能指网站的响应时间。通常我们会用QPSTPSRT并发数吞吐量更进一步还会对比CPU负载来衡量一个系统的性能。当然我们知道一个系统的吞吐量和响应时间跟外部网络分布式架构等都存在强关联性能优化也跟各级缓存设计数据冗余等架构有很大关系假设其他方面我们都已经完成了聚焦到本文我们暂时关心的是单节点的性能优化。毕竟一屋不扫何以扫天下整体系统的优化也有赖于各个节点的调优。从感官上来谈当请求量很少的时候我们可以很轻松的通过各种缓存优化来提高响应时间。但是随着用户激增请求次数的增加我们的服务也对应着需要并发模型来支撑。但是一个节点的并发量有个上限当达到这个上限后响应时间就会变长所以我们需要探索并发到什么程度才是最优的才能保证最高的并发数同时响应时间又能保持在理想情况。由于我们暂时不关注节点以外的网络情况那么下文我们特指的RT是指服务接收到请求后完成计算返回计算结果经历的时间。单线程单线程情况下服务接收到请求后开始初始化资源准备计算返回结果时间主要花在CPU计算和CPU外的IO等待时间多个请求来也只能排队一个一个来那么RT计算如下RT T(cpu) T(io)QPS 1000ms / RT多线程单线程情况很好计算多线程情况就复杂了我们目标是计算出最佳并发量也就是线程数N单核情况N [T(cpu) T(io)] / T(cpu)M核情况N [T(cpu) T(io)] / T(cpu) * M由于多核情况CPU未必能全部使用存在一个资源利用百分比P那么并发的最佳线程数 N [T(cpu) T(io)] / T(cpu) * M * P吞吐量我们知道单线程的QPS很容易算出来那么多线程的QPSQPS 1000ms / RT * N 1000ms / [T(cpu) T(io)] * [T(cpu) T(io)] / T(cpu) * M * P 1000ms / T(cpu) * M * P在机器核数固定情况下也即是并发模式下最大的吞吐量跟服务的CPU处理时间和CPU利用率有关。CPU利用率不高就是通常我们听到最多的抱怨压测时候qps都打满了但是cpu的load就是上不去。并发模型中多半个共享资源有关而共享资源又跟锁息息相关那么大部分时候我们想对节点服务做性能调优时就是对锁的优化这个下一节会提到。前面我们是假设机器核数固定的情况下做优化的那假如我们把缓存IO锁都优化了剩下的还有啥空间去突破呢回想一下我们谈基础理论的时候提到的Amdahl定律公式之前已经给出该定律想表达的结论是随着核数或者处理器个数的增加可以增加优化加速比但是会达到上限而且增加趋势愈发不明显。9.2锁优化说真的我们并不喜欢锁的只不过由于临界资源的存在不得已为之。如果业务上设计能避免出现临界资源那就没有锁优化什么事了。但是锁优化的一些原则还是要说一说的。时间既然我们并不喜欢锁那么就按需索取只在核心的同步块加锁用完立马释放减少锁定临界区的时间这样就可以把资源竞争的风险降到最低。粒度进一步看有时候我们核心同步块可以进一步分离比如只读的情况下并不需要加锁这时候就可以用读写锁各自的读写功能。还有一种情况有时候我们反而会小心翼翼的到处加锁来防止意外出现可能出现三个同步块加了三个锁这也造成CPU的过多停顿根据业务其实可以把相关逻辑合并起来也就是锁粗化。锁的分离和粗化具体还得看业务如何操作。尺度除了锁暂用时间和粒度外还有就是锁的尺度还是根据业务来能用共享锁定的情况就不要用独享锁。死锁这个不用说都知道死锁防不胜防我们前面也介绍很多现成的工具比如可重入锁还有线程本地变量等方式都可以一定程度避免死锁。9.3JVM锁机制我们在代码层面把锁的应用都按照安全法则做到最好了那接下来要做的就是下钻到JVM级别的锁优化。具体实现原理我们暂不展开后续有机会再搞个专题写写JVM锁实现。自旋锁(Spin Lock)自旋锁的原理非常简单。如果持有锁的线程可以在短时间内释放锁资源那么等待竞争锁的那些线程不需要在内核状态和用户状态之间进行切换。它只需要等待并且锁可以在释放锁之后立即获得锁。这可以避免消耗用户线程和内核切换。但是自旋锁让CPU空等着什么也不干也是一种浪费。如果自旋锁的对象一直无法获得临界资源则线程也无法在没有执行实际计算的情况下一致进行CPU空转因此需要设置自旋锁的最大等待时间。如果持有锁的线程在旋转等待的最大时间没有释放锁则自旋锁线程将停止旋转进入阻塞状态。JDK1.6开启自旋锁 -XX:UseSpinning1.7之后控制器收回到JVM自主控制。偏向锁(Biased Lock)偏向锁偏向于第一个访问锁的线程如果在运行过程中同步锁只有一个线程访问不存在多线程争用的情况则线程是不需要触发同步的这种情况下就会给线程加一个偏向锁。如果在运行过程中遇到了其他线程抢占锁则持有偏向锁的线程会被挂起JVM会消除它身上的偏向锁将锁恢复到标准的轻量级锁。JDK1.6开启偏向锁 -XX:UseBiasedLocking1.7之后控制器收回到JVM自主控制。轻量级锁(Lightweight Lock)轻量级锁是由偏向锁升级来的偏向锁运行在一个线程进入同步块的情况下当第二个线程加入锁竞争的时候偏向锁就会升级为轻量级锁。重量级锁(Heavyweight Lock)如果锁检测到与另一个线程的争用则锁定会膨胀至重量级锁。也就是我们常规用的同步修饰产生的同步作用。9.4无锁最后其实我想说的是虽然锁很符合我们人类的逻辑思维设计起来也相对简单但是摆脱不了临界区的限制。那么我们不妨换个思路进入无锁的时间也就是我们可能会增加业务复杂度的情况下来消除锁的存在。CAS策略著名的CAS(Compare And Swap)是多线程中用于实现同步的原子指令。它将内存位置的内容与给定值进行比较并且只有它们相同时才将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。原子性保证了新值是根据最新信息计算出来的; 如果在此期间该值已被另一个线程更新则写入将失败。操作的结果必须表明它是否进行了替换; 这可以通过简单的Boolean来响应或通过返回从内存位置读取的值而不是写入它的值来完成。也就是一个原子操作包含了要操作的数据和给定认为正确的值进行对比一致的话就继续不一致则会重试。这样就在没有锁的情况下完成并发操作。我们知道原子类 AtomicInteger内部实现的原理就是采用了CAS策略来完成的。AtomicInteger.java 132/** * Atomically sets the value to the given updated value * if the current value {code } the expected value. * * param expect the expected value * param update the new value * return {code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}类似的还有AtomicReference.java 115/** * Atomically sets the value to the given updated value * if the current value {code } the expected value. * param expect the expected value * param update the new value * return {code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update);}有兴趣的同学可以再了解一下Unsafe的实现进一步可以了解Distuptor无锁框架。十、并发模型前面我们大费周章的从并发的基础概念到多线程的使用方法和优化技巧。但都是战术层面的本节我们试着从战略的高度来扩展一下并发编程的世界。可能大多数情况下我们谈并发都会想到多线程但是本节我们要打破这种思维在完全不用搞多线程那一套的情况下实现并发。首先我们用”多线程模式“来回顾前文所讲的所有关于Thread衍生出来的定义开发和优化的技术。多线程模式单位线程完成完整的任务也即是一条龙服务线程。优势映射现实单一任务便于理解和编码劣势有状态多线程共享资源导致资源竞争死锁问题线程等待阻塞失去并发意义有状态多线程非阻塞算法有利减少竞争提升性能但难以实现多线程执行顺序无法预知流水线模型介绍完传统多线程工作模式后我们来学习另外一种并发模式传统的多线程工作模式理解起来很直观接下来我们要介绍的另外一种并发模式看起来就不那么直观了。流水线模型特点是无状态线程无状态也意味着无需竞争共享资源无需等待也就是非阻塞模型。流水线模型顾名思义就是流水线上有多个环节每个环节完成自己的工作后就交给下一个环节无需等待上游周而复始地完成自己岗位上的一亩三分地。各个环节之间交付无需等待完成即可交付。而工厂的流水线也不止一条所以有多条流水线同时工作。不同岗位的生产效率是不一样的所以不同流水线之间也可以发生协同。我们说流水线模型也称为响应式模型或者事件驱动模型其实就是流水线上游岗位完成生产就通知下游岗位所以完成了一个事件的通知每完成一次就通知一下就是响应式的意思。流水线模型总体的思想就是纵向切分任务把任务里面耗时过久的环节单独隔离出来避免完成一个任务需要耗费等待的时间。在实现上又分为Actors和Channels模型Actors该模型跟我们讲述的流水线模型基本一致可以理解为响应式模型Channels由于各个环节直接不直接交互所以上下游之间并不知道对方是谁好比不同环节直接用的是几条公共的传送带来接收物品各自只需要把完成后的半成品扔到传送带即使后面流水线优化了去掉中间的环节对于个体岗位来说也是无感知的它只是周而复始的从传送带拿物品来加工。流水线的优缺点优势无共享状态无需考虑资源抢占死锁等问题独享内存worker可以持有内存合并多次操作到内存后再持久化提升效率贴合底层单线程模式贴合硬件运行流程便于代码维护任务顺序可预知劣势不够直观一个任务被拆分为流水线上多个环节代码层面难以直观理解业务逻辑由于流水线模式跟人类的顺序执行思维不一样比较费解那么有没有办法让我们编码的时候像写传统的多线程代码一样而运行起来又是流水线模式呢答案是肯定的比如基于Java的Akka/Reator/Vert.x/Play/Qbit框架或者golang就是为流水线模式而生的并发语言还有nodeJS等等。流水线模型的开发实践可以参考流水线模型实践。其实流水线模型背后用的也还是多线程来实现只不过对于传统多线程模式下我们需要小心翼翼来处理跟踪资源共享问题而流水线模式把以前一个线程做的事情拆成多个每一个环节再用一条线程来完成避免共享线程直接通过管道传输消息。这一块展开也是一个专题主要设计NIONetty和Akka的编程实践先占坑后面补上。函数式模型函数式并行模型类似流水线模型单一的函数是无状态的所以避免了资源竞争的复杂度同时每个函数类似流水线里面的单一环境彼此直接通过函数调用传递参数副本函数之外的数据不会被修改。函数式模式跟流水线模式相辅相成逐渐成为更为主流的并发架构。具体的思想和编程实践也是个大专题篇幅限制本文就先不展开拟在下个专题中详细介绍《函数式编程演化》。十一、总结由于CPU和I/O天然存在的矛盾传统顺序的同步工作模式导致任务阻塞CPU空等着没有执行浪费资源。多线程为突破了同步工作模式的情况下浪费CPU资源即使单核情况下也能将时间片拆分成单位给更多的线程来轮询享用。多线程在不同享状态的情况下非常高效不管协同式还是抢占式都能在单位时间内执行更多的任务从而更好的榨取CPU资源。但是多数情况下线程之间是需要通信的这一核心场景导致了一系列的问题也就是线程安全。内存被共享的单位由于被不同线程轮番读取写入操作这种操作带来的后果往往是写代码的人类没想到的也就是并发带来的脏数据等问题。解决了资源使用效率问题又带来了新的安全问题如何解决悲观方式就是对于存在共享内存的场景无论如何只同意同一时刻一个线程操作也就是同步操作方法或者代码段或者显示加锁。或者volatile来使共享的主存跟每条线程的工作内存同步每次读都从主存刷新每次写完都刷到主存要保证线程安全1、不要使用多线程2、多线程各干各的不要共享内存3、共享的内存空间是不可变的常量final4、实在要变每次变完要同步到主存volatile依赖当前值的逻辑除外5、原子变量6、根据具体业务避免脏数据这块就是多线程最容易犯错的地方线程安全后要考虑的就是效率问题如果不解决效率问题那还干嘛要多线无限
高并发编程知识体系
一、问题1、什么是线程的交互方式2、如何区分线程的同步/异步阻塞/非阻塞3、什么是线程安全如何做到线程安全4、如何区分并发模型5、何谓响应式编程6、操作系统如何调度多线程二、关键词同步异步阻塞非阻塞并行并发临界区竞争条件指令重排锁amdahl,gustafson三、全文概要上一篇我们介绍分布式系统的知识体系《分布式架构知识体系》由于单机的性能上限原因我们才不得不发展分布式技术。那么话说回来如果单机的性能没能最大限度的榨取出来就盲目建设分布式系统那就有点本末倒置了。而且上一篇我们给的忠告是如果有可能的话不要用分布式意思是说如果单机性能满足的话就不要折腾复杂的分布式架构。如果说分布式架构是宏观上的性能扩展那么高并发则是微观上的性能调优这也是上一篇性能部分拆出来的大专题。本文将从线程的基础理论谈起逐步探究线程的内存模型线程的交互线程工具和并发模型的发展。扫除关于并发编程的诸多模糊概念从新构建并发编程的层次结构。四、基础理论4.1基本概念开始学习并发编程前我们需要熟悉一些理论概念。既然我们要研究的是并发编程那首先应该对并发这个概念有所理解才是而说到并发我们肯定要要讨论一些并行。并发一个处理器同时处理多个任务并行多个处理器或者是多核的处理器同时处理多个不同的任务然后我们需要再了解一下同步和异步的区别同步执行某个操作开始后就一直按部就班等着直到操作结束异步执行某个操作后立即离开后面有响应的话再来通知执行者接着我们再了解一个重要的概念临界区公共资源或者共享数据由于共享数据的出现必然会导致竞争所以我们需要再了解一下阻塞某个操作需要的共享资源被占用了只能等待称为阻塞非阻塞某个操作需要的共享资源被占用了不等待立即返回并携带错误信息回去期待重试如果两个操作都在等待某个共享资源而且都互不退让就会造成死锁死锁参考著名的哲学家吃饭问题饥饿饥饿的哲学家等不齐筷子吃饭活锁相互谦让而导致阻塞无法进入下一步操作跟死锁相反死锁是相互竞争而导致的阻塞4.2并发级别理想情况下我们希望所有线程都一起并行飞起来。但是CPU数量有限线程源源不断总得有个先来后到不同场景需要的并发需求也不一样比如秒杀系统我们需要很高的并发程度但是对于一些下载服务我们需要的是更快的响应并发反而是其次的。所以我们也定义了并发的级别来应对不同的需求场景。阻塞阻塞是指一个线程进入临界区后其它线程就必须在临界区外等待待进去的线程执行完任务离开临界区后其它线程才能再进去。无饥饿线程排队先来后到不管优先级大小先来先执行就不会产生饥饿等待资源也即公平锁相反非公平锁则是根据优先级来执行有可能排在前面的低优先级线程被后面的高优先级线程插队就形成饥饿无障碍共享资源不加锁每个线程都可以自有读写单监测到被其他线程修改过则回滚操作重试直到单独操作成功风险就是如果多个线程发现彼此修改了所有线程都需要回滚就会导致死循环的回滚中造成死锁无锁无锁是无障碍的加强版无锁级别保证至少有一个线程在有限操作步骤内成功退出不管是否修改成功这样保证了多个线程回滚不至于导致死循环无等待无等待是无锁的升级版并发编程的最高境界无锁只保证有线程能成功退出但存在低级别的线程一直处于饥饿状态无等待则要求所有线程必须在有限步骤内完成退出让低级别的线程有机会执行从而保证所有线程都能运行提高并发度。4.3量化模型首先多线程不意味着并发但并发肯定是多线程或者多进程。我们知道多线程存在的优势是能够更好地利用资源有更快的请求响应。但是我们也深知一旦进入多线程附带而来的是更高的编码复杂度线程设计不当反而会带来更高的切换成本和资源开销。但是总体上我们肯定知道利大于弊这不是废话吗不然谁还愿意去搞多线程并发程序但是如何衡量多线程带来的效率提升呢我们需要借助两个定律来衡量。AmdahlS1/(1-aa/n)其中a为并行计算部分所占比例n为并行处理结点个数。这样当1-a0时(即没有串行只有并行)最大加速比sn当a0时即只有串行没有并行最小加速比s1当n→∞时极限加速比s→ 1/1-a这也就是加速比的上限。Gustafson系统优化某部件所获得的系统性能的改善程度取决于该部件被使用的频率或所占总执行时间的比例。两面列举了这两个定律来衡量系统改善后提升效率的量化指标具体的应用我们在下文的线程调优会再详细介绍。五、内存模型宏观上分布式系统需要解决的首要问题是数据一致性同样微观上并发编程要解决的首要问题也是数据一致性。貌似我们搞了这么多年的斗争都是在攻关一致性这个世界性难题。既然并发编程要从微观开始那么我们肯定要对CPU和内存的工作机理有所了解尤其是数据在CPU和内存直接的传输机制。5.1整体原则探究内存模型之前我们要抛出三个概念:原子性在32位的系统中对于4个字节32位的Integer的操作对应的JVM指令集映射到汇编指令为一个原子操作所以对Integer类型的数据操作是原子性但是Long类型为8个字节64位32位系统要分为两条指令来操作所以不是原子操作。对于32位操作系统来说单次操作能处理的最长长度为32bit而long类型8字节64bit所以对long的读写都要两条指令才能完成即每次读写64bit中的32bit可见性线程修改变量对其他线程即时可见有序性串行指令顺序唯一并行线程直接指令可能出现不一致也即指令被重排。而指令重排也是有一定原则(摘自《深入理解Java虚拟机第12章》)程序次序规则一个线程内按照代码顺序书写在前面的操作先行发生于书写在后面的操作锁定规则一个unLock操作先行发生于后面对同一个锁的lock操作volatile变量规则对一个变量的写操作先行发生于后面对这个变量的读操作传递规则如果操作A先行发生于操作B而操作B又先行发生于操作C则可以得出操作A先行发生于操作C线程启动规则Thread对象的start()方法先行发生于此线程的每个一个动作线程中断规则对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终结规则线程中所有的操作都先行发生于线程的终止检测我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行对象终结规则一个对象的初始化完成先行发生于他的finalize()方法5.2逻辑内存我们谈的逻辑内存也即是JVM的内存格局。JVM将操作系统提供的物理内存和CPU缓存在逻辑分为堆栈方法区和程序计数器。在《从宏观微观角度浅析JVM虚拟机》 一文我们详细介绍了JVM的内存模型分布并发编程我们主要关注的是堆栈的分配因为线程都是寄生在栈里面的内存段把栈里面的方法逻辑读取到CPU进行运算。5.3物理内存而实际的物理内存包含了主存和CPU的各级缓存还有寄存器而为了计算效率CPU往往会就近从缓存里面读取数据。在并发的情况下就会造成多个线程之间对共享数据的错误使用。5.4内存映射由于可能发生对象的变量同时出现在主存和CPU缓存中就可能导致了如下问题线程修改的变量对外可见读写共享变量时出现竞争资源由于线程内的变量对栈外是不可见的但是成员变量等共享资源是竞争条件所有线程可见就会出现如下当一个线程从主存拿了一个变量1修改后变成2存放在CPU缓存还没来得及同步回主存时另外一个线程又直接从主存读取变量为1这样就出现了脏读。现在我们弄清楚了线程同步过程数据不一致的原因接下来要解决的目标就是如何避免这种情况的发生经过大量的探索和实践我们从概念上不断的革新比如并发模型的流水线化和无状态函数式化而且也提供了大量的实用工具。接下来我们从无到有先了解最简单的单个线程的一些特点弄清楚一个线程有多少能耐后才能深刻认识多个线程一起打交道会出现什么幺蛾子。六、线程单元6.1状态我们知道应用启动体现的就是静态指令加载进内存进而进入CPU运算操作系统在内存开辟了一段栈内存用来存放指令和变量值从而形成了进程。而其实我们的JVM也就是一个进程而线程是进程的最小单位也就是说进程是由很多个线程组成的。而由于进程的上下文关联的变量引用计数器等现场数据占用了打段的内存空间所以频繁切换进程需要整理一大段内存空间来保存未执行完的进程现场等下次轮到CPU时间片再恢复现场进行运算。这样既耗费时间又浪费空间所以我们才要研究多线程。毕竟由于线程干的活毕竟少工作现场数据毕竟少所以切换起来比较快而且暂用少量空间。而线程切换直接也需要遵守一定的法则不然到时候把工作现场破坏了就无法恢复工作了。线程状态我们先来研究线程的生命周期看看Thread类里面对线程状态的定义就知道public enum State { /** * Thread state for a thread which has not yet started. */ NEW,/** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE,/** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {link Object#wait() Object.wait}. */ BLOCKED,/** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * ul * li{link Object#wait() Object.wait} with no timeout/li * li{link #join() Thread.join} with no timeout/li * li{link LockSupport#park() LockSupport.park}/li * /ul * * pA thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called ttObject.wait()/tt * on an object is waiting for another thread to call * ttObject.notify()/tt or ttObject.notifyAll()/tt on * that object. A thread that has called ttThread.join()/tt * is waiting for a specified thread to terminate. */ WAITING,/** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * ul * li{link #sleep Thread.sleep}/li * li{link Object#wait(long) Object.wait} with timeout/li * li{link #join(long) Thread.join} with timeout/li * li{link LockSupport#parkNanos LockSupport.parkNanos}/li * li{link LockSupport#parkUntil LockSupport.parkUntil}/li * /ul */ TIMED_WAITING,/** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED;}生命周期线程的状态NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED。注释也解释得很清楚各个状态的作用而各个状态的转换也有一定的规则需要遵循的。6.2动作介绍完线程的状态和生命周期接下来了解线程具备哪些常用的操作。首先线程也是一个普通的对象Thread所有的线程都是Thread或者其子类的对象。那么这个内存对象被创建出来后就会放在JVM的堆内存空间当我们执行start()方法的时候对象的方法体在栈空间分配好对应的栈帧来往执行引擎输送指令也即是方法体翻译成JVM的指令集。线程操作新建线程new Thread()新建一个线程对象内存为线程在栈上分配好内存空间启动线程start()告诉系统系统准备就绪只要资源允许随时可以执行栈里面的指令执行线程run()分配了CPU等计算资源正在执行栈里面的指令集停止线程(过时)stop()把CPU和内存资源回收线程消亡由于太过粗暴已经被标记为过时线程中断interrupt()中断是对线程打上了中断标签可供run()里面的方法体接收中断信号至于线程要不要中断全靠业务逻辑设计而不是简单粗暴的把线程直接停掉isInterrupt()主要是run()方法体来判断当前线程是否被置为中断interrupted()静态方法也是用户判断线程是否被置为中断状态同时判断完将线程中断状态复位线程休眠sleep()静态方法线程休眠指定时间段此间让出CPU资源给其他线程但是线程依然持有对象锁其他线程无法进入同步块休眠完成后也未必立刻执行需要等到资源允许才能执行线程等待(对象方法)wait()是Object的方法也即是对象的内置方法在同步块中线程执行到该方法时也即让出了该对象的锁所以无法继续执行线程通知(对象方法)notify(),notifyAll()此时该对象持有一个或者多个线程的wait调用notify()随机的让一个线程恢复对象的锁调用notifyAll()则让所有线程恢复对象锁线程挂起(过时)suspend()线程挂起并没有释放资源而是只能等到resume()才能继续执行线程恢复(过时)resume()由于指令重排可能导致resume()先于suspend()执行导致线程永远挂起所以该方法被标为过时线程加入join()在一个线程调用另外一个线程的join()方法表明当前线程阻塞直到被调用线程执行结束再进行线程让步yield()暂停当前线程进而执行别的线程当前线程等待下一轮资源允许再进行防止该线程一直霸占资源而其他线程饿死线程等待park()基于线程对象的操作较对象锁更为精准线程恢复unpark(Thread thread)对应park()解锁为不可重入锁线程分组为了管理线程于是有了线程组的概念业务上把类似的线程放在一个ThreadGroup里面统一管理。线程组表示一组线程此外线程组还可以包括其他线程组。线程组形成一个树其中除了初始线程组以外的每个线程组都有一个父线程。线程被允许访问它自己的线程组信息但不能访问线程组的父线程组或任何其他线程组的信息。守护线程通常情况下线程运行到最后一条指令后则完成生命周期结束线程然后系统回收资源。或者单遇到异常或者return提前返回但是如果我们想让线程常驻内存的话比如一些监控类线程需要24小时值班的于是我们又创造了守护线程的概念。setDaemon()传入true则会把线程一直保持在内存里面除非JVM宕机否则不会退出。线程优先级线程优先级其实只是对线程打的一个标志但并不意味这高优先级的一定比低优先级的先执行具体还要看操作系统的资源调度情况。通常线程优先级为5边界为[1,10]。 /** * The minimum priority that a thread can have. */ public final static int MIN_PRIORITY 1;/** * The default priority that is assigned to a thread. */ public final static int NORM_PRIORITY 5;/** * The maximum priority that a thread can have. */ public final static int MAX_PRIORITY 10;本节介绍了线程单元的转态切换和常用的一些操作方法。如果只是单线程的话其他都没必要研究这些重头戏在于多线程直接的竞争配合操作下一节则重点介绍多个线程的交互需要关注哪些问题。七、线程交互其实上一节介绍的线程状态切换和线程操作都是为线程交互做准备的。不然如果只是单线程完全没必要搞什么通知恢复让步之类的操作了。7.1交互方式线程交互也就是线程直接的通信最直接的办法就是线程直接通信传值而间接方式则是通过共享变量来达到彼此的交互。等待释放对象锁允许其他线程进入同步块通知重新获取对象锁继续执行中断状态交互通知其他线程进入中断织入合并线程多个线程合并为一个7.2线程安全我们最关注的还是通过共享变量来达到交互的方式。线程如果都各自干活互不搭理的话自然相安无事但多数情况下线程直接需要打交道而且需要分享共享资源那么这个时候最核心的就是线程安全了。什么是线程安全当多个线程访问同一个对象时如果不用考虑这些线程在运行时环境下的调度和交替运行也不需要进行额外的同步或者在调用方进行任何其他的协调操作调用这个对象的行为都可以获取正确的结果那这个对象是线程安全的。摘自《深入Java虚拟机》如何保证线程安全我们最早接触线程安全可能是JDK提供的一些号称线程安全的容器比如Vetor较ArrayList是线程安全HashTable较HashMap是线程安全。其实线程安全类并不代表也不等同线程安全的程序而线程不安全的类同样可以完成线程安全的程序。我们关注的也就是写出线程安全的程序那么如何写出线程安全的代码呢下面列举了线程安全的主要设计技术无状态这个有点函数式编程的味道下文并发模式会介绍到总之就是线程只有入参和局部变量如果变量是引用的话确保变量的创建和调用生命周期都发生在线程栈内就可以确保线程安全。无共享状态完全要求线程无状态比较难实现必要的状态是无法避免的那么我们就必须维护不同线程之间的不同状态这可是个麻烦事。幸好我们有ThreadLocal这个神器该对象跟当前线程绑定而且只对当前线程可见完美解决了无共享状态的问题。不可变状态最后实在没办法避免状态共享在线程之间共享状态最怕的就是无法确保能维护好正确的读写顺序而且多线程确实也无法正确维护好这个共享变量。那么我们索性粗暴点把共享的状态定位不可变比如价格final修饰一下这样就达到安全状态共享。消息传递一个线程通常也不是所有步骤都需要共享状态而是部分环节才需要的那么我们把共享状态的代码拆开无共享状态的那部分自然不用关心而共享状态的小段代码则通过加入消息组件来传递状态。这个设计到并发模式的流水线编程模式下文并发模式会重点介绍。线程安全容器JUC里面提供大量的并发容器涉及到线程交互的时候使用安全容器可以避免大部分的错误而且大大降低了代码的复杂度。通过synchronized给方法加上内置锁来实现线程安全的类如VectorHashTable,StringBufferAtomicXXX如AtomicIntegerConcurrentXXX如ConcurrentHashMapBlockingQueue/BlockingDequeCopyOnWriteArrayList/CopyOnWriteArraySetThreadPoolExecutorsynchronized同步该关键字确保代码块同一时间只被一个线程执行在这个前提下再设计符合线程安全的逻辑其作用域为对象对象加锁进入同步代码块之前获取对象锁实例方法对象加锁执行实例方法前获取对象实例锁类方法类加锁执行类方法前获取类锁volatile约束volatile确保每次操作都能强制同步CPU缓存和主存直接的变量。而且在编译期间能阻止指令重排。读写并发情况下volatile也不能确保线程安全上文解析内存模型的时候有提到过。这节我们论述了编写线程安全程序的指导思想其中我们提到了JDK提供的JUC工具包下一节将重点介绍并发编程常用的趁手工具。八、线程工具前文我们介绍了内存理论和线程的一些特征大家都知道并发编程容易出错而且出了错还不好调试排查幸好JDK里面集成了大量实用的API工具我们能熟悉这些工具写起并发程序来也事半功倍。工具篇其实就是对锁的不断变种适应更多的开发场景提高性能提供更方便的工具从最粗暴的同步修饰符到灵活的可重入锁到宽松的条件接着到允许多个线程访问的信号量最后到读写分离锁。8.1同步控制由于大多数的并发场景都是需要访问到共享资源的为了保证线程安全我们不得已采用锁的技术来做同步控制这节我们介绍的是适用不同场景各种锁技术。ReentrantLock可重入互斥锁具有与使用synchronized的隐式监视器锁具有相同的行为和语义但具有更好扩展功能。ReentrantLock由最后成功锁定的线程拥有而且还未解锁。当锁未被其他线程占有时线程调用lock()将返回并且成功获取锁。如果当前线程已拥有锁则该方法将立即返回。这可以使用方法isHeldByCurrentThread()和getHoldCount()来检查。构造函数接受可选的fairness参数。当设置为true时在竞争条件下锁定有利于赋予等待时间最长线程的访问权限。否则锁将不保证特定的访问顺序。在多线程访问的情况使用公平锁比默认设置有着更低的吞吐量但是获得锁的时间比较小而且可以避免等待锁导致的饥饿。但是锁的公平性并不能保证线程调度的公平性。因此使用公平锁的许多线程中的一个可以连续多次获得它而其他活动线程没有进展并且当前没有持有锁。不定时的tryLock方法不遵循公平性设置。即使其他线程正在等待如果锁可用它也会成功。任意指定锁的起始位置中断响应锁申请等待限时tryLock()公平锁ConditionCondition从拥有监控方法wait,notify,notifyAll的Object对象中抽离出来成为独特的对象高效的让每个对象拥有更多的等待线程。和锁对比起来如果说用Lock代替synchronized那么Condition就是用来代替Object本身的监控方法。Condition实例跟Object本身的监控相似同样提供wait()方法让调用的线程暂时挂起让出资源知道其他线程通知该对象转态变化才可能继续执行。Condition实例来源于Lock实例通过Lock调用newCondition()即可。Condition较Object原生监控方法可以保证通知顺序。Semaphore锁和同步块同时只能允许单个线程访问共享资源这个明显有些单调部分场景其实可以允许多个线程访问这个时候信号量实例就派上用场了。信号量逻辑上维持了一组许可证 线程调用acquire()阻塞直到许可证可用后才能执行。执行release()意味着释放许可证实际上信号量并没有真正的许可证只是采用了计数功能来实现这个功能。ReadWriteLock顾名思义读写锁将读写分离细化了锁的粒度照顾到性能的优化。CountDownLatch这个锁有点“关门放狗”的意思尤其在我们压测的时候模拟实时并行请求该实例将线程积累到指定数量后调用countDown()方法让所有线程同时执行。CyclicBarrierCyclicBarrier是加强版的CountDownLatch上面讲的是一次性“关门放狗”而循环栅栏则是集齐了指定数量的线程在资源都允许的情况下同时执行然后下一批同样的操作周而复始。LockSupportLockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。因为park() 和 unpark()有许可的存在调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。8.2线程池线程池总览线程多起来的话就需要管理不然就会乱成一锅粥。我们知道线程在物理上对应的就是栈里面的一段内存存放着局部变量的空间和待执行指令集。如果每次执行都要从头初始化这段内存然后再交给CPU执行效率就有点低了。假如我们知道该段栈内存会被经常用到那我们就不要回收创建完就让它在栈里面呆着要用的时候取出来用完换回去是不是就省了初始化线程空间的时间这样是我们搞出线程池的初衷。其实线程池很简单就是搞了个池子放了一堆线程。既然我们搞线程池是为了提高效率那就要考虑线程池放多少个线程比较合适太多了或者太少了有什么问题怎么拒绝多余的请求除了异常怎么处理。首先我们来看跟线程池有关的一张类图。线程池归结起来就是这几个类的使用技巧了重点关注ThreadPoolExecutor和Executors即可。创建线程池万变不离其宗创建线程池的各种马甲方法最后都是调用到这方法里面包含核心线程数最大线程数线程工厂拒绝策略等参数。其中线程工厂则可以实现自定义创建线程的逻辑。public interface ThreadFactory { Thread newThread(Runnable r);}创建的核心构造方法ThreadPoolExecutor.java 1301 /** * Creates a new {code ThreadPoolExecutor} with the given initial * parameters. * * param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {code allowCoreThreadTimeOut} is set * param maximumPoolSize the maximum number of threads to allow in the * pool * param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * param unit the time unit for the {code keepAliveTime} argument * param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {code Runnable} * tasks submitted by the {code execute} method. * param threadFactory the factory to use when the executor * creates a new thread * param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * throws IllegalArgumentException if one of the following holds:br * {code corePoolSize 0}br * {code keepAliveTime 0}br * {code maximumPoolSize 0}br * {code maximumPoolSize corePoolSize} * throws NullPointerException if {code workQueue} * or {code threadFactory} or {code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)拒绝策略包含 /** 实际上并未真正丢弃任务但是线程池性能会下降 * A handler for rejected tasks that runs the rejected task * directly in the calling thread of the {code execute} method, * unless the executor has been shut down, in which case the task * is discarded. */ public static class CallerRunsPolicy implements RejectedExecutionHandler/** 粗暴停止抛异常 * A handler for rejected tasks that throws a * {code RejectedExecutionException}. */ public static class AbortPolicy implements RejectedExecutionHandler /** 悄无声息的丢弃拒绝的任务 * A handler for rejected tasks that silently discards the * rejected task. */ public static class DiscardPolicy implements RejectedExecutionHandler /** 丢弃最老的请求 * A handler for rejected tasks that discards the oldest unhandled * request and then retries {code execute}, unless the executor * is shut down, in which case the task is discarded. */ public static class DiscardOldestPolicy implements RejectedExecutionHandler 包括Executors.java中的创建线程池的方法具体实现也是通过ThreadPoolExecutor来创建的。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueueRunnable());}public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable());}public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);}public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable()));}调用线程池ThreadPoolExecutor.java 1342/** 同步执行线程出现异常打印堆栈信息 * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {code RejectedExecutionHandler}. * * param command the task to execute * throws RejectedExecutionException at discretion of * {code RejectedExecutionHandler}, if the task * cannot be accepted for execution * throws NullPointerException if {code command} is null */public void execute(Runnable command)/*** 异步提交线程任务出现异常无法同步追踪堆栈,本质上也是调用execute()方法*/public T FutureT submit(Runnable task, T result) { if (task null) throw new NullPointerException(); RunnableFutureT ftask newTaskFor(task, result); execute(ftask); return ftask;}线程池优化线程池已经是我们使用线程的一个优化成果了而线程池本身的优化其实就是根据实际业务选择好不同类型的线程池预估并发线程数量控制好线程池预留线程数最大线程数一般设为2N1最好N是CPU核数这些涉及CPU数量核数还有具体业务。另外我们还注意到ForkJoinPool继承了AbstractExecutorService这是在JDK7才加上去的目的就是提高任务派生出来更多任务的执行效率由上图的继承关系我们可以知道跟普通线程池最大的差异是执行的任务类型不同。public void execute(ForkJoinTask? task) { if (task null) throw new NullPointerException(); externalPush(task);}public void execute(Runnable task) { if (task null) throw new NullPointerException(); ForkJoinTask? job; if (task instanceof ForkJoinTask?) // avoid re-wrap job (ForkJoinTask?) task; else job new ForkJoinTask.RunnableExecuteAction(task); externalPush(job); }8.3并发容器其实我们日常开发大多数并发场景直接用JDK 提供的线程安全数据结构足矣下面列举了常用的列表集合等容器具体就不展开讲相信大家都用得很熟悉了。ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueueBlockingQueueConcurrentSkipListMapVectorHashTable...九、线程调优9.1性能指标回想一下当我们在谈性能优化的时候我们可能指的是数据库的读写次数也可能指网站的响应时间。通常我们会用QPSTPSRT并发数吞吐量更进一步还会对比CPU负载来衡量一个系统的性能。当然我们知道一个系统的吞吐量和响应时间跟外部网络分布式架构等都存在强关联性能优化也跟各级缓存设计数据冗余等架构有很大关系假设其他方面我们都已经完成了聚焦到本文我们暂时关心的是单节点的性能优化。毕竟一屋不扫何以扫天下整体系统的优化也有赖于各个节点的调优。从感官上来谈当请求量很少的时候我们可以很轻松的通过各种缓存优化来提高响应时间。但是随着用户激增请求次数的增加我们的服务也对应着需要并发模型来支撑。但是一个节点的并发量有个上限当达到这个上限后响应时间就会变长所以我们需要探索并发到什么程度才是最优的才能保证最高的并发数同时响应时间又能保持在理想情况。由于我们暂时不关注节点以外的网络情况那么下文我们特指的RT是指服务接收到请求后完成计算返回计算结果经历的时间。单线程单线程情况下服务接收到请求后开始初始化资源准备计算返回结果时间主要花在CPU计算和CPU外的IO等待时间多个请求来也只能排队一个一个来那么RT计算如下RT T(cpu) T(io)QPS 1000ms / RT多线程单线程情况很好计算多线程情况就复杂了我们目标是计算出最佳并发量也就是线程数N单核情况N [T(cpu) T(io)] / T(cpu)M核情况N [T(cpu) T(io)] / T(cpu) * M由于多核情况CPU未必能全部使用存在一个资源利用百分比P那么并发的最佳线程数 N [T(cpu) T(io)] / T(cpu) * M * P吞吐量我们知道单线程的QPS很容易算出来那么多线程的QPSQPS 1000ms / RT * N 1000ms / [T(cpu) T(io)] * [T(cpu) T(io)] / T(cpu) * M * P 1000ms / T(cpu) * M * P在机器核数固定情况下也即是并发模式下最大的吞吐量跟服务的CPU处理时间和CPU利用率有关。CPU利用率不高就是通常我们听到最多的抱怨压测时候qps都打满了但是cpu的load就是上不去。并发模型中多半个共享资源有关而共享资源又跟锁息息相关那么大部分时候我们想对节点服务做性能调优时就是对锁的优化这个下一节会提到。前面我们是假设机器核数固定的情况下做优化的那假如我们把缓存IO锁都优化了剩下的还有啥空间去突破呢回想一下我们谈基础理论的时候提到的Amdahl定律公式之前已经给出该定律想表达的结论是随着核数或者处理器个数的增加可以增加优化加速比但是会达到上限而且增加趋势愈发不明显。9.2锁优化说真的我们并不喜欢锁的只不过由于临界资源的存在不得已为之。如果业务上设计能避免出现临界资源那就没有锁优化什么事了。但是锁优化的一些原则还是要说一说的。时间既然我们并不喜欢锁那么就按需索取只在核心的同步块加锁用完立马释放减少锁定临界区的时间这样就可以把资源竞争的风险降到最低。粒度进一步看有时候我们核心同步块可以进一步分离比如只读的情况下并不需要加锁这时候就可以用读写锁各自的读写功能。还有一种情况有时候我们反而会小心翼翼的到处加锁来防止意外出现可能出现三个同步块加了三个锁这也造成CPU的过多停顿根据业务其实可以把相关逻辑合并起来也就是锁粗化。锁的分离和粗化具体还得看业务如何操作。尺度除了锁暂用时间和粒度外还有就是锁的尺度还是根据业务来能用共享锁定的情况就不要用独享锁。死锁这个不用说都知道死锁防不胜防我们前面也介绍很多现成的工具比如可重入锁还有线程本地变量等方式都可以一定程度避免死锁。9.3JVM锁机制我们在代码层面把锁的应用都按照安全法则做到最好了那接下来要做的就是下钻到JVM级别的锁优化。具体实现原理我们暂不展开后续有机会再搞个专题写写JVM锁实现。自旋锁(Spin Lock)自旋锁的原理非常简单。如果持有锁的线程可以在短时间内释放锁资源那么等待竞争锁的那些线程不需要在内核状态和用户状态之间进行切换。它只需要等待并且锁可以在释放锁之后立即获得锁。这可以避免消耗用户线程和内核切换。但是自旋锁让CPU空等着什么也不干也是一种浪费。如果自旋锁的对象一直无法获得临界资源则线程也无法在没有执行实际计算的情况下一致进行CPU空转因此需要设置自旋锁的最大等待时间。如果持有锁的线程在旋转等待的最大时间没有释放锁则自旋锁线程将停止旋转进入阻塞状态。JDK1.6开启自旋锁 -XX:UseSpinning1.7之后控制器收回到JVM自主控制。偏向锁(Biased Lock)偏向锁偏向于第一个访问锁的线程如果在运行过程中同步锁只有一个线程访问不存在多线程争用的情况则线程是不需要触发同步的这种情况下就会给线程加一个偏向锁。如果在运行过程中遇到了其他线程抢占锁则持有偏向锁的线程会被挂起JVM会消除它身上的偏向锁将锁恢复到标准的轻量级锁。JDK1.6开启偏向锁 -XX:UseBiasedLocking1.7之后控制器收回到JVM自主控制。轻量级锁(Lightweight Lock)轻量级锁是由偏向锁升级来的偏向锁运行在一个线程进入同步块的情况下当第二个线程加入锁竞争的时候偏向锁就会升级为轻量级锁。重量级锁(Heavyweight Lock)如果锁检测到与另一个线程的争用则锁定会膨胀至重量级锁。也就是我们常规用的同步修饰产生的同步作用。9.4无锁最后其实我想说的是虽然锁很符合我们人类的逻辑思维设计起来也相对简单但是摆脱不了临界区的限制。那么我们不妨换个思路进入无锁的时间也就是我们可能会增加业务复杂度的情况下来消除锁的存在。CAS策略著名的CAS(Compare And Swap)是多线程中用于实现同步的原子指令。它将内存位置的内容与给定值进行比较并且只有它们相同时才将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。原子性保证了新值是根据最新信息计算出来的; 如果在此期间该值已被另一个线程更新则写入将失败。操作的结果必须表明它是否进行了替换; 这可以通过简单的Boolean来响应或通过返回从内存位置读取的值而不是写入它的值来完成。也就是一个原子操作包含了要操作的数据和给定认为正确的值进行对比一致的话就继续不一致则会重试。这样就在没有锁的情况下完成并发操作。我们知道原子类 AtomicInteger内部实现的原理就是采用了CAS策略来完成的。AtomicInteger.java 132/** * Atomically sets the value to the given updated value * if the current value {code } the expected value. * * param expect the expected value * param update the new value * return {code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}类似的还有AtomicReference.java 115/** * Atomically sets the value to the given updated value * if the current value {code } the expected value. * param expect the expected value * param update the new value * return {code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update);}有兴趣的同学可以再了解一下Unsafe的实现进一步可以了解Distuptor无锁框架。十、并发模型前面我们大费周章的从并发的基础概念到多线程的使用方法和优化技巧。但都是战术层面的本节我们试着从战略的高度来扩展一下并发编程的世界。可能大多数情况下我们谈并发都会想到多线程但是本节我们要打破这种思维在完全不用搞多线程那一套的情况下实现并发。首先我们用”多线程模式“来回顾前文所讲的所有关于Thread衍生出来的定义开发和优化的技术。多线程模式单位线程完成完整的任务也即是一条龙服务线程。优势映射现实单一任务便于理解和编码劣势有状态多线程共享资源导致资源竞争死锁问题线程等待阻塞失去并发意义有状态多线程非阻塞算法有利减少竞争提升性能但难以实现多线程执行顺序无法预知流水线模型介绍完传统多线程工作模式后我们来学习另外一种并发模式传统的多线程工作模式理解起来很直观接下来我们要介绍的另外一种并发模式看起来就不那么直观了。流水线模型特点是无状态线程无状态也意味着无需竞争共享资源无需等待也就是非阻塞模型。流水线模型顾名思义就是流水线上有多个环节每个环节完成自己的工作后就交给下一个环节无需等待上游周而复始地完成自己岗位上的一亩三分地。各个环节之间交付无需等待完成即可交付。而工厂的流水线也不止一条所以有多条流水线同时工作。不同岗位的生产效率是不一样的所以不同流水线之间也可以发生协同。我们说流水线模型也称为响应式模型或者事件驱动模型其实就是流水线上游岗位完成生产就通知下游岗位所以完成了一个事件的通知每完成一次就通知一下就是响应式的意思。流水线模型总体的思想就是纵向切分任务把任务里面耗时过久的环节单独隔离出来避免完成一个任务需要耗费等待的时间。在实现上又分为Actors和Channels模型Actors该模型跟我们讲述的流水线模型基本一致可以理解为响应式模型Channels由于各个环节直接不直接交互所以上下游之间并不知道对方是谁好比不同环节直接用的是几条公共的传送带来接收物品各自只需要把完成后的半成品扔到传送带即使后面流水线优化了去掉中间的环节对于个体岗位来说也是无感知的它只是周而复始的从传送带拿物品来加工。流水线的优缺点优势无共享状态无需考虑资源抢占死锁等问题独享内存worker可以持有内存合并多次操作到内存后再持久化提升效率贴合底层单线程模式贴合硬件运行流程便于代码维护任务顺序可预知劣势不够直观一个任务被拆分为流水线上多个环节代码层面难以直观理解业务逻辑由于流水线模式跟人类的顺序执行思维不一样比较费解那么有没有办法让我们编码的时候像写传统的多线程代码一样而运行起来又是流水线模式呢答案是肯定的比如基于Java的Akka/Reator/Vert.x/Play/Qbit框架或者golang就是为流水线模式而生的并发语言还有nodeJS等等。流水线模型的开发实践可以参考流水线模型实践。其实流水线模型背后用的也还是多线程来实现只不过对于传统多线程模式下我们需要小心翼翼来处理跟踪资源共享问题而流水线模式把以前一个线程做的事情拆成多个每一个环节再用一条线程来完成避免共享线程直接通过管道传输消息。这一块展开也是一个专题主要设计NIONetty和Akka的编程实践先占坑后面补上。函数式模型函数式并行模型类似流水线模型单一的函数是无状态的所以避免了资源竞争的复杂度同时每个函数类似流水线里面的单一环境彼此直接通过函数调用传递参数副本函数之外的数据不会被修改。函数式模式跟流水线模式相辅相成逐渐成为更为主流的并发架构。具体的思想和编程实践也是个大专题篇幅限制本文就先不展开拟在下个专题中详细介绍《函数式编程演化》。十一、总结由于CPU和I/O天然存在的矛盾传统顺序的同步工作模式导致任务阻塞CPU空等着没有执行浪费资源。多线程为突破了同步工作模式的情况下浪费CPU资源即使单核情况下也能将时间片拆分成单位给更多的线程来轮询享用。多线程在不同享状态的情况下非常高效不管协同式还是抢占式都能在单位时间内执行更多的任务从而更好的榨取CPU资源。但是多数情况下线程之间是需要通信的这一核心场景导致了一系列的问题也就是线程安全。内存被共享的单位由于被不同线程轮番读取写入操作这种操作带来的后果往往是写代码的人类没想到的也就是并发带来的脏数据等问题。解决了资源使用效率问题又带来了新的安全问题如何解决悲观方式就是对于存在共享内存的场景无论如何只同意同一时刻一个线程操作也就是同步操作方法或者代码段或者显示加锁。或者volatile来使共享的主存跟每条线程的工作内存同步每次读都从主存刷新每次写完都刷到主存要保证线程安全1、不要使用多线程2、多线程各干各的不要共享内存3、共享的内存空间是不可变的常量final4、实在要变每次变完要同步到主存volatile依赖当前值的逻辑除外5、原子变量6、根据具体业务避免脏数据这块就是多线程最容易犯错的地方线程安全后要考虑的就是效率问题如果不解决效率问题那还干嘛要多线无限