#1,避免死锁前端
死锁问题是多线程的特有问题,它能够认为是线程间切换消耗系统性能的一种极端状况。java
在死锁时,线程间相互等待资源,而又不释放自身的资源,致使无穷无尽的等待,其结果是系统任务永远没法执行完成。数据结构
死锁问题是在多线程开发中应该坚定避免和杜绝的问题。多线程
通常来讲,要出现死锁问题须要知足如下条件:并发
@互斥条件:一个资源每次只能被一个线程使用。app
@请求与保持条件:一个线程因请求资源而阻塞时,对已得到的资源保持不放。less
@不剥夺条件:线程已得到的资源,在未使用完以前,不能强行剥夺。
async
@循环等待条件:若干线程之间造成一种头尾相接的循环等待资源关系。函数
只要破坏死锁4个必要条件中的任何一个,死锁问题就能得以解决。高并发
#2,减少锁持有时间
对于使用锁进行并发控制的应用程序而言,在锁竞争过程当中,单个线程对锁的持有时间与系统性能有着直接的关系。
若是线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。所以,在程序开发过程当中,应该尽量地减小对某个锁的占有时间,以减小线程间互斥的可能。
public synchronized void syncMethod(){ method1();//比较耗时 coreMethod(); method2();//比较耗时 }
在syncMethod()方法中,假设只有coreMethod()方法是有同步须要的,而method1()和method2()分别是重量级的方法,则会花费较长的CPU时间。此时,若是并发量大,使用这种对整个方法作同步的方案,会致使等待线程大量增长。由于一个线程,在进入该方法时得到内部锁,只有再全部任务都执行完成后,才会释放锁。
一个较为优化的解决方法时,只在必要时进行同步,这样就能明显减小线程持有锁的时间,提升系统的吞吐量。
public void syncMethod(){ method1();//比较耗时 synchronized(this){ coreMethod(); } method2();//比较耗时 }
在改进的代码中,只针对coreMethod()方法作了同步,锁占用的时间相对较短,所以能有更高的并行度。
减小锁的持有时间有助于下降锁冲突的可能性,进而提高系统的并发能力。
#3.减少锁粒度
减少锁粒度也是一种削弱多线程锁竞争的一种有效手段,这种技术典型的使用场景就是ConcurrentHashMap类的实现。
做为JDK并发包中重要的成员类,很好地使用了拆分锁对象的方式提升ConcurrentHashMap的吞吐量。ConcurrentHashMap将整个HashMap分红若干个段(Segment),每一个段都是一个子HashMap。
若是须要在ConcurrentHashMap中增长一个新的表项,并非将整个HashMap加锁,而是首先根据hashcode获得该表项应该被存放到哪一个段中,而后对该段加锁,并完成put()操做。在多线程环境中,若是多个线程同时进行put()操做,只要被加入的表项不存放在同一个段中,则线程间即可以作到真正的并行。
默认状况下,ConcurrentHashMap拥有16个段,所以,若是够幸运的话,ConcurrentHashMap能够同时接受16个线程同时插入(若是都插入不一样的段中),从而大大提升其吞吐量。
可是,减小锁粒度会引入一个新的问题,即:当系统须要取得全局锁时,其消耗的资源会比较多。仍然以ConcurrentHashMap类为例,虽然其put()方法很好地分离了锁,可是当试图访问ConcurrentHashMap全局信息时,就须要同时取得全部段的锁方能顺利实施。好比ConcurrentHashMap的size()方法,它将返回ConcurrentHashMap的有效表项的数量,即ConcurrentHashMap的所有有效表项之和。要获取这个信息须要取得全部字段的锁,所以,可参考其size()的代码以下:
public int size() { final Segment<K,V>[] segments = this.segments; long sum = 0; long check = 0; int[] mc = new int[segments.length]; // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { check = 0; sum = 0; int mcsum = 0; for (int i = 0; i < segments.length; ++i) { sum += segments[i].count; mcsum += mc[i] = segments[i].modCount; } if (mcsum != 0) { for (int i = 0; i < segments.length; ++i) { check += segments[i].count; if (mc[i] != segments[i].modCount) { check = -1; // force retry break; } } } if (check == sum) break; } if (check != sum) { // Resort to locking all segments sum = 0; for (int i = 0; i < segments.length; ++i) segments[i].lock(); for (int i = 0; i < segments.length; ++i) sum += segments[i].count; for (int i = 0; i < segments.length; ++i) segments[i].unlock(); } if (sum > Integer.MAX_VALUE) return Integer.MAX_VALUE; else return (int)sum; }
能够看到代码在后面计算总数时,先要得到全部段的锁,而后再求和。可是,ConcurrentHashMap的size()方法并不老是这样执行,事实上,size()方法会先使用无锁的方式求和,若是失败才会尝试这种加锁方法。但无论怎么说,在高并发场合ConcurrentHashMap的size()的性能依然要差于同步的HashMap.
所以,只有再相似于size()获取全局信息的方法调用并不频繁时,这种减少锁粒度的方法才能真正意义上提升系统吞吐量。
所谓减少锁粒度,就是指缩小锁定对象的范围,(能够锁定实例对象的,就不锁定class类对象)从而减小锁冲突的可能性,进而提升系统的并发能力。
#4,读写分离锁类替换独占锁
使用读写分离锁来替代独占锁是减少锁粒度的一种特殊状况。若是说上面的减小锁粒度是经过分割数据结构实现的,那么,读写锁则是对系统功能点的分割。
由于读操做自己不会影响数据的完整性和一致性,所以,理论上讲,在大部分状况下,应该能够容许多线程同时读。
在读多写少的场合,使用读写锁能够有效提高系统的并发能力。
#5,锁分离
读写锁思想的延伸就是锁分离,读写锁根据读写操做功能上的不一样,进行了有效的锁分离。依据应用程序的功能特色,使用相似的分离思想,也能够对独占锁进行分离。
以LinkedBlockingQueue来讲,take()和put()分别实现了从队列中取得数据和往队列中增长数据的功能。虽然两个函数都对当前队列进行了修改操做,但因为LinkedBlockingQueue是基于链表的,所以,两个操做分别做用于队列的前端和尾端,从理论上说,二者并不冲突。
若是使用独占锁,则要求在两个操做进行时获取当前队列的独占锁,那么take()和put()操做就不可能真正的并发,在运行时,它们会彼此等待对方释放锁资源。在这种状况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能。
所以,在JDK实现中,并无采用这样的方式,取而代之的是两把不一样的锁分离了take()和put()操做。
/** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition();
以上代码片断定义了takeLock和putLock,所以take()和put()函数相互独立,它们之间不存在锁竞争关系。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
/** * Inserts the specified element at the tail of this queue, waiting if * necessary for space to become available. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from * capacity. Similarly for all other uses of count in * other wait guards. */ while (count.get() == capacity) { notFull.await(); } enqueue(e); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
经过takeLock和putLock两把锁,LinkedBlockingQueue实现了取数据和写数据的分离,使二者在真正意义上成为可并发的操做。
#6,重入锁(ReentrantLock)和内部锁(synchronized)
内部锁和重入锁有功能上的重复,全部使用内部锁实现的功能,使用重入锁均可以实现。从使用上看,内部锁使用简单,所以获得了普遍的使用,重入锁使用略微复杂,必须在finally代码中,显示释放重入锁,而内部锁能够自动释放。
从性能上看,在高并发量的状况下,内部锁的性能略逊于重入锁,可是JVM对内部锁实现了不少优化,而且有理由相信,在未来的JDK版本中,内部锁的性能会愈来愈好。
从功能上看,重入锁有着更为强大的功能,好比提供了锁等待时间,支持锁终端和快读锁轮询,这些技术有助于避免死锁的产生,从而提升系统的稳定性。
同时,冲如梭还提供了一套Condition机制,经过Condition,重入锁能够进行复杂的线程控制功能,而相似的功能,内部锁须要经过Object的wait()和notify()方法实现。
#7,锁粗化
一般状况下,为了保证多线程间的有效并发,会要求每一个线程尺有所短寸有所长的时间尽可能短,即在使用完公共资源后,应该当即释放锁。只有这样,等待在这个锁上的其余线程才能尽早地得到资源执行任务。可是,凡事都有一个度,若是堆同一个锁不停地进行请求,同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能的优化。
为此,JVM在遇到一连串连续的对同一锁不断进行请求和释放的操做时,便会把全部的锁操做整合成对锁的一次请求,从而减小对锁的请求同步次数,这个操做叫作锁的粗化。
public void syncMethod(){ synchronized(lock){ //TODO } synchronized(lock){ //TODO }
上面的代码会被整合为以下形式:
public void syncMethod(){ synchronized(lock){ //TODO //TODO } }
在开发当中,咱们也应该有意识地在合理的场合进行锁的粗化,尤为当在循环内请求锁时。
for (int i = 0; i < CIRCLE; i++) { synchronized(lock){ //TODO } }
将上面的代码粗化为:
synchronized(lock){ for (int i = 0; i < CIRCLE; i++) { //TODO } }
#8,自旋锁
在上面已经提到,线程的状态和上下文切换是要消耗系统资源的。在多线程比并发时,频繁的挂起和恢复线程的操做会给系统带来极大的压力。特别是当访问共享资源仅需花费很小一段CPU时间时,锁的等待可能只须要很短的时间,这段时间可能要比将线程挂起并恢复的时间还要短,所以,为了这段时间去作重量级的线程切换是不值得的。
为此,JVM引入了自旋锁。自旋锁可使线程在没有取得锁时,不被挂起,而转而去执行一个空循环,在若干个空循环后,线程若是得到了锁,则继续执行。若线程依然不能得到锁,才会被挂起。
使用自旋锁后,线程被挂起的概率相对减小,线程执行的连贯性相对增强。所以,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,是有必定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,每每依然没法得到对应的锁,不只仅白白浪费了CPU时间,最终仍是免不了执行被挂起的操做,反而浪费了系统资源。
JVM虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。
#9,锁消除
锁消除是JVM在即时编译时,经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。经过锁消除,能够节省毫无心义的请求锁时间。
在Java软件开发过程当中,开发人员必然会使用一些JDK的内置API,好比StringBuffer,Vector等。这些经常使用的工具类可能会被大面积地使用。虽然这些工具类自己可能有对应的非同步版本,可是开发人员也颇有可能在彻底没有多线程竞争的场合使用他们。
在这种状况下,这些工具类内部的同步方法就是没必要要的。JVM虚拟机能够在运行时,基于逃逸分析技术,捕获到这些不可能存在竞争却有申请锁的代码段,并消除这些没必要要的锁,从而提升系统性能。
public void testStringBuffer(String a, String b){ StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); return sb.toString(); }
sb变量的做用域仅限于方法体内部,不可能逃逸出该方法,一次它就不可能被多个线程同时访问。
逃逸分析和锁消除分别可使用-XX:+DoEscapeAnalysis和-XX:+EliminateLocks开启(锁消除必须工做再-server模式下)。
对锁的请求和释放是要消耗系统资源的。使用锁消除即便能够去掉那些不可能存在多线程访问的锁请求,从而提升系统性能。
#10,锁偏向
锁偏向是JDK1.6提出的一种锁优化方式。其核心思想是,若是程序没有竞争,则取消以前已经取得锁的线程同步操做。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需再进行相关的同步操做,从而节省了操做时间,若是在此之间有其余线程进行了锁请求,则锁退出偏向模式。在JVM中使用-XX:+UseBiasedLocking能够设置启用偏向锁。
偏向锁在锁竞争激烈的场合没有优化效果,由于大量的竞争会致使持有锁的线程不停地切换,锁也很难一致保持在偏向模式,此时,使用锁偏向不只得不到性能的提高,反而有损系统性能。所以,在激烈竞争的场合,使用-XX:-UseBiasedLocking参数禁用锁偏向反而能提高系统吞吐量。