“锁”的竞争必然会导致程序的整体性能下降,以下就是为了降低这种辐作用的建议:
1.减小锁持有时间
如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。程序开发应该尽可能地减少对某个锁的占有时间,以减少线程间互斥的可能.
public synchronized void syncMethod(){
othercode1();
mutextMethod();
othercode2();}
优化后:
public void syncMethod2(){
othercode1();
synchronized(this){
mutextMethod();
}
othercode2();}
注意:减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。
2.减小锁粒度
减小锁粒度也是一种削弱多线程锁竞争的有效手段。这种技术典型的使用场景就是Concurren-tHashMap类的实现。
ConcurrentHashMap实现原理:
对于HashMap来说,最重要的两个方法就是get()和put()。一种最自然的想法就是对整个HashMap加锁,必然可以得到一个线程安全的对象。但是这样做,我们就认为加锁粒度太大。对于ConcurrentHashMap,它内部进一步细分了若干个小的HashMap,称之为段(SEGMENT)。默认情况下,一个ConcurrentHashMap被进一步细分为16个段。
如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程间便可以做到真正的并行。由于默认有16个段,因此,如果够幸运的话,ConcurrentHashMap可以同时接受16个线程同时插入(如果都插入不同的段中),从而大大提供其吞吐量。
根据key,获得对应的段的序号。接着得到段,然后将数据插入给定的段中。
但是,减少锁粒度会引入一个新的问题,即:当系统需要取得全局锁时,其消耗的资源会比较多。
比如ConcurrentHashMap的size()方法,要获取这个信息需要取得所有子段的锁,因此,只有在类似于size()获取全局信息的方法调用并不频繁时,这种减小锁粒度的方法才能真正意义上提高系统吞吐量。
注意:所谓减少锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提高系统的并发能力。
3.读写分离锁来替换独占锁
使用读写锁ReadWriteLock可以提高系统的性能。使用读写分离锁来替代独占锁是减小锁粒度的一种特殊情况。如果减少锁粒度是通过分割数据结构实现的,那么,读写锁则是对系统功能点的分割。
在读多写少的场合,读写锁对系统性能是很有好处的。因为如果系统在读写数据时均只使用独占锁,那么读操作和写操作间、读操作和读操作间、写操作和写操作间均不能做到真正的并发,并且需要相互等待。而读操作本身不会影响数据的完整性和一致性。因此,理论上讲,在大部分情况下,应该可以允许多线程同时读,读写锁正是实现了这种功能。
注意:在读多写少的场合,使用读写锁可以有效提升系统的并发能力。
4.锁分离
如果将读写锁的思想做进一步的延伸,就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。依据应用程序的功能特点,使用类似的分离思想,也可以对独占锁进行分离。
一个典型的案例就是java.util.concurrent.LinkedBlock-ingQueue的实现,采用两把不同的锁,分离了take()和put()操作。
5.锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早地获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。
那么在开发过程中,大家也应该有意识地在合理的场合进行锁的粗化,尤其当在循环内请求锁时。
for(int i=0;i<CIRCLE;i++){
synchronized(lock){
} }
优化后:
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}}
注意:性能优化就是根据运行时的真实情况对各个资源点进行权衡折中的过程。锁粗化的思想和减少锁持有时间是相反的,但在不同的场合,它们的效果并不相同。所以大家需要根据实际情况,进行权衡。
Java虚拟机对锁的优化
1.锁偏向
锁偏向是一种针对加锁操作的优化手段。它的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。因此,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,其效果不佳。因为在竞争激烈的场合,最有可能的情况是每次都是不同的线程来请求相同的锁。这样偏向模式会失效,因此还不如不启用偏向锁。使用Java虚拟机参数-XX:+UseBiasedLocking可以开启偏向锁。
2.轻量级锁
如果偏向锁失败,虚拟机并不会立即挂起线程。它还会使用一种称为轻量级锁的优化手段。轻量级锁的操作也很轻便,它只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
3.自旋锁
锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做最后的努力——自旋锁。由于当前线程暂时无法获得锁,但是什么时候可以获得锁是一个未知数。也许在几个CPU时钟周期后,就可以得到锁。如果这样,简单粗暴地挂起线程可能是一种得不偿失的操作。因此,系统会进行一次赌注:它会假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真实地将线程在操作系统层面挂起。
4.锁消除
锁消除是一种更彻底的锁优化。Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
我们必然会使用一些JDK的内置API,比如StringBuffer、Vector等。你在使用这些类的时候,也许根本不会考虑这些对象到底内部是如何实现的。比如,你很有可能在一个不可能存在并发竞争的场合使用Vector。而众所周知,Vector内部使用了synchronized请求锁。如果虚拟机检测到这种情况,就会将这些无用的锁操作去除。
锁消除涉及的一项关键技术为逃逸分析。所谓逃逸分析就是观察某一个变量是否会逃出某一个作用域。
比如:
public String[] createStrings(){
Vector<String> v=new Vector<String>();
for(int i=0;i<100;i++){
v.add(Integer.toString(i));
}
return v.toArray(new String[]{});
}
如果creat-eStrings()返回的不是String数组,而是v本身,那么就认为变量v逃逸出了当前函数,也就是说v有可能被其他线程访问。如果是这样,虚拟机就不能消除v中的锁操作。
逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数可以打开锁消除。