避免死锁
死锁出现的四要素:
- 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放;
- 请求与保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放
- 不可中条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
- 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
减小锁的持有时间
减小锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。举例:synchronized 同步方法块,而不是整个方法。
减小锁粒度
所谓的减少锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提高系统的并发能力。
举例:ConcurentHashmap 中使用分段锁提高 put() 操作的并发能力,默认情况下 ConcurentHashmap 有16个段,理想情况下,它可以同时接受16个线程同时插入。
减小锁引入的问题:当需要获取全局锁时,其消耗的资源会比较多。如 ConcurentHashmap 中的 size() 方法需要同时获取所有段的锁方能顺利实施(当然首先会使用无锁方式获取,失败时采用加锁方式获取)。
实现锁分离
根据读写操作功能的不同,进行有效的锁分离。如分别使用读锁和写锁,读锁之间是相容的,即对象可以持有多个读锁;对象对写锁的占用是独占式的,只有在对象没有锁的情况下才能获取对象的写锁。
举例:LinkedBlockingQueue
在 LinkedBlockingQueue 的实现中,take()和put()函数分别从队列中取得数据和往队列中增加数据,JDK使用了两把不同的锁分离了这个两个操作,使得两者在真正意义上成为可并发的操作。
重入锁和内部锁
重入锁比内部锁功能更强大,但内部锁使用更简单。JDK1.7后两者之间的性能已经差不了太多了,且内部锁将会是JDK以后优化的重点,所以建议优先使用内部锁。
锁粗化
虚拟机在遇到一连串连续对同一锁不断请求和释放的操作时,会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步数,这个操作叫做锁的粗化。
public void demo(){
synchronized(lock){
//do something
}
//其他非同步操作
synchronized(lock){
//do something
}
}
锁粗化后:
public void demo(){
synchronized(lock){
//do something
//其他非同步操作
}
}
又如:循环中的加锁操作(循环体执行很快),应对整个循环加锁,可降低锁的请求。
使用原子操作类
使用java.util.concurrent.atomic下的原子操作类,替换有锁的操作,原子操作类的基础是CAS(Compare And Swap),该操作比基于锁的方式拥有更优越的性能,大部分的线程处理器都已经支持原子化的CAS指令。
JVM层面
自旋锁(Spinning Lock)
锁的等待只需要很短的时间,这段时间可能比线程挂起并恢复的时间还要短,因此JVM引入了自旋锁。
自旋锁可以使线程没有取得锁时,不被挂起,而转去执行一个空循环,在若干个空循环后,线程如果获得了锁,则继续执行,若线程依然没有获得锁,才会被挂起,避免用户线程和内核的切换的消耗。
在JVM中使用 -XX:UseSpinning 参数来开启自旋锁,使用 -XX:PreBlockSpin 来设置自旋锁的等待次数。
锁消除(Lock Elimination)
通过对上下文的扫描,去除不可能存在共享资源竞争的锁,节省毫无意义的请求锁时间。
举例:StringBuffer、Vector这些同步类用于了没有多线程竞争的场合。
锁偏向(Biased Lock)
如果程序没有竞争,则取消之前已经获取锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁,无需在进行相关的同步操作,如果在此之间有其他线程进行了锁请求,则锁退出偏向模式。
偏向锁在锁竞争激烈的场合没有优化效果,因为大量的竞争会导致持有锁的进程不停地切换,锁也很难一直保持在偏向模式,使用 -XX:-UseBiasedLocking=false 禁用偏向锁。