我曾经对本身的小弟说,若是你实在搞不清楚何时用HashMap,何时用ConcurrentHashMap,那么就用后者,你的代码bug会不多。java
他问我:ConcurrentHashMap是什么? -.-golang
编程不是炫技。大多数状况下,怎么把代码写简单,才是能力。web
多线程生来就是复杂的,也是容易出错的。一些难以理解的概念,要规避。本文不讲基础知识,由于你手里就有jdk的源码。 算法
第一类就是Thread
类。你们都知道有两种实现方式。第一能够继承Thread
覆盖它的run
方法;第二种是实现Runnable
接口,实现它的run
方法;而第三种建立线程的方法,就是经过线程池。编程
咱们的具体代码实现,就放在run方法中。api
咱们关注两种状况。一个是线程退出条件,一个是异常处理状况。tomcat
有的run方法执行完成后,线程就会退出。但有的run方法是永远不会结束的。结束一个线程确定不是经过Thread.stop()
方法,这个方法已经在java1.2版本就废弃了。因此咱们大致有两种方式控制线程。安全
定义退出标志放在while中bash
代码通常长这样。网络
private volatile boolean flag= true;
public void run() {
while (flag) {
}
}
复制代码
标志通常使用volatile
进行修饰,使其读可见,而后经过设置这个值来控制线程的运行,这已经成了约定俗成的套路。
使用interrupt方法终止线程
相似这种。
while(!isInterrupted()){……}
复制代码
对于InterruptedException
,好比Thread.sleep所抛出的,咱们通常是补获它,而后静悄悄的忽略。中断容许一个可取消任务来清理正在进行的工做,而后通知其余任务它要被取消,最后才终止,在这种状况下,此类异常须要被仔细处理。
interrupt
方法不必定会真正”中断”线程,它只是一种协做机制。interrupt方法一般不能中断一些处于阻塞状态的I/O操做。好比写文件,或者socket传输等。这种状况,须要同时调用正在阻塞操做的close方法,才可以正常退出。
interrupt系列使用时候必定要注意,会引入bug,甚至死锁。
java中会抛出两种异常。一种是必需要捕获的,好比InterruptedException,不然没法经过编译;另一种是能够处理也能够不处理的,好比NullPointerException等。
在咱们的任务运行中,颇有可能抛出这两种异常。对于第一种异常,是必须放在try,catch中的。但第二种异常若是不去处理的话,会影响任务的正常运行。
有不少同窗在处理循环的任务时,没有捕获一些隐式的异常,形成任务在遇到异常的状况下,并不能继续执行下去。若是不能肯定异常的种类,能够直接捕获Exception或者更通用的Throwable。
while(!isInterrupted()){
try{
……
}catch(Exception ex){
……
}
}
复制代码
java中实现同步的方式有不少,大致分为如下几种。
synchronized
关键字ReentrantLock
volatile
关键字生产者、消费者是wait、notify最典型的应用场景,这些函数的调用,是必需要放在synchronized代码块里才可以正常运行的。它们同信号量同样,大多数状况下属于炫技,对代码的可读性影响较大,不推荐。关于ObjectMonitor
相关的几个函数,只要搞懂下面的图,就基本ok了。
使用ReentrantLock最容易发生错误的就是忘记在finally代码块里关闭锁。大多数同步场景下,使用Lock就足够了,并且它还有读写锁的概念进行粒度上的控制。咱们通常都使用非公平锁,让任务自由竞争。非公平锁性能高于公平锁性能,非公平锁能更充分的利用cpu的时间片,尽可能的减小cpu空闲的状态时间。非公平锁还会形成饿死现象:有些任务一直获取不到锁。
synchronized经过锁升级机制,速度不见得就比lock慢。并且,经过jstack,可以方便的看到其堆栈,使用仍是比较普遍。
volatile老是能保证变量的读可见,但它的目标是基本类型和它锁的基本对象。假如是它修饰的是集合类,好比Map,那么它保证的读可见是map的引用,而不是map对象,这点必定要注意。
synchronized和volatile都体如今字节码上(monitorenter、monitorexit),主要是加入了内存屏障。而Lock,是纯粹的java api。
ThreadLocal很方便,每一个线程一份数据,也很安全,但要注意内存泄露。假如线程存活时间长,咱们要保证每次使用完ThreadLocal,都调用它的remove()方法(具体来讲是expungeStaleEntry),来清除数据。
concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
最全的线程池大概有7个参数,想要合理使用线程池,确定不会不会放过这些参数的优化。
concurrent包最经常使用的就是线程池,日常工做建议直接使用线程池,Thread类就能够下降优先级了。咱们经常使用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类建立。
newSingleThreadExecutor能够用于快速建立一个异步线程,很是方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。
我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 复制代码
假如个人任务能够预估,corePoolSize,maximumPoolSize通常都设成同样大的,而后存活时间设的特别的长。能够避免线程频繁建立、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不同的,通常I/O密集型的应用线程就能够开的多一些。
threadFactory我通常也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,可以直观的看到我所建立的线程。
高并发下的线程池,最好可以监控起来。可使用日志、存储等方式保存下来,对后续的问题排查帮助很大。
一般,能够经过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。
最容易被遗忘的可能就是线程的饱和策略了。也就是线程和缓冲队列的空间所有用完了,新加入的任务将如何处置。jdk默认实现了4种策略,默认实现的是AbortPolicy
,也就是直接抛出异常。下面介绍其余几种。
DiscardPolicy
比abort更加激进,直接丢掉任务,连异常信息都没有。
CallerRunsPolicy
由调用的线程来处理这个任务。好比一个web应用中,线程池资源占满后,新进的任务将会在tomcat线程中运行。这种方式可以延缓部分任务的执行压力,但在更多状况下,会直接阻塞主线程的运行。
DiscardOldestPolicy
丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程)。
不少状况下,这些饱和策略可能并不能知足你的需求,你能够自定义本身的策略,好比将任务持久化到一些存储中。
阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提升的编码的灵活性,很是方便。在并发编程中,通常推荐使用阻塞队列,这样实现能够尽可能地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。
ArrayBlockingQueue对访问者的调用默认是不公平的,咱们能够经过设置构造方法参数将其改为公平阻塞队列。
LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用作线程池队列的时候,会比较危险。
SynchronousQueue是一个不存储元素的阻塞队列。每个put操做必须等待一个take操做,不然不能继续添加元素。队列自己不存储任何元素,吞吐量很是高。对于提交的任务,若是有空闲线程,则使用空闲线程来处理;不然新建一个线程来处理任务”。它更像是一个管道,在一些通信框架中(好比rpc),一般用来快速处理某个请求,应用较为普遍。
DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象须要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式一般可以比大多数非阻塞的while循环更加节省cpu资源。
另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜想它的用途。在线程池的构造参数中,咱们使用的队列,必定要注意其特性和边界。好比,即便是最简单的newFixedThreadPool,在某些场景下,也是不安全的,由于它使用了无界队列。
假若有一堆接口A-Y,每一个接口的耗时最大是200ms,最小是100ms。
个人一个服务,须要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?
此类问题典型的还有赛马问题,只有经过并行计算才能完成问题。归结起来能够分为两类:
在concurrent包出现以前,须要手工的编写这些同步过程,很是复杂。如今就可使用CountDownLatch和CyclicBarrier进行便捷的编码。
CountDownLatch是经过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了本身的任务后,计数器的值就会减1。当计数器值到达0时,它表示全部的线程已经完成了任务,而后在闭锁上等待的线程就能够恢复执行任务。 CyclicBarrier与其相似,能够实现一样的功能。不过在平常的工做中,使用CountDownLatch会更频繁一些。
Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽可能少用。
信号量能够实现限流的功能,但它只是经常使用限流方式的一种。其余两种是漏桶算法、令牌桶算法。
hystrix的熔断功能,也有使用信号量进行资源的控制。
在Java中,对于Lock和Condition能够理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。
但这些类和函数对于初中级码农来讲,难以理解,容易产生bug,应该在业务代码中严格禁止。但在网络编程、或者一些框架类工程中,这些功能是必须的,万不可将这部分的工做随便分配给某个小弟。
无论是wait、notify,仍是同步关键字或者锁,能不用就不用,由于它们会引起程序的复杂性。最好的方式,是直接使用concurrent包所提供的机制,来规避一些编码方面的问题。
concurrent包中的CAS概念,在必定程度上算是无锁的一种实现。更专业的有相似disruptor的无锁队列框架,但它依然是创建在CAS的编程模型上的。近些年,相似AKKA这样的事件驱动模型正在走红,但编程模型简单,不表明实现简单,背后的工做依然须要多线程去协调。
golang引入协程(coroutine)概念之后,对多线程加入了更加轻量级的补充。java中能够经过javaagent技术加载quasar补充一些功能,但我以为你不会为了这丁点效率去牺牲编码的可读性。