减少锁持有时间
1 | public synchronized void syncMethod() { |
syncMethod()方法中,假设只有mutextMethod()方法是有同步需要的,如果othercode1()和othercode2()分别是重量级的方法,则会花费较长的CPU时间.
一个较为优化的解决方案是,只在必要时进行同步,这样就能明显减少线程持有锁的时间,提高系统的吞吐量;
1 | public void syncMethod() { |
注意:减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力
减小锁粒度
这种技术典型的使用场景就是ConcurrentHashMap.
对于HashMap来说,最重要的两个方法就是get() 和put(),一种最自然的想法就是对整个HashMap加锁,必然可以得到一个线程安全的对象.但是这样做,我们就认为加锁粒度太大.对于ConcurrentHashMap,它内部进一步细分了若干个小的hashMap,称之为段(SEGMENT).默认的情况下,一个ConcurrentHashMap被进一步细分为16个段
如果需要在ConcurrentHashMap中增加一个新的表项,并不是整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作.在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程间便可以做到真正的并行
注意:
所谓减小锁粒度,就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力。
但减小锁粒度会引入一个新的问题,当需要获取全局锁的时候,其消耗的资源会较多,不如concurrenthashMap的size()方法.事实上计算size的时候会先使用无锁的方式计算,如果失败会采用这个方法,但是在高并发的场合concurrenthashmap的size依然要差于同步的hashmap.因此在类似于size获取全局信息方法调用不频繁的情况下,这种减小粒度的的方法才是真正意义上的提高系统并发量
读写分离锁来替换独占锁
使用读写分离锁来替代独占锁是减少锁粒度的一种特殊情况。如果说上述的减小锁粒度是通过分割数据结构实现的,那么读写分离锁则是对系统功能点的分割。
注意:在读多写少的场合,使用读写锁可以有效提升系统的并发能力
锁分离
如果将读写锁的思想进一步的延伸,就是锁分离.读写锁根据读写锁操作功能上的不同,进行了有效的锁分离.使用类似的思想,也可以对独占锁进行分离.以LinkedBlockingQueue为例,take函数和put函数分别实现了冲队列取和往队列加数据,虽然两个方法都对队列进项了修改,但是LinkedBlockingQueue是基于链表的所以一个操作的是头,一个是队列尾端,从理论情况下将并不冲突。
如果使用独占锁则take和put就不能完成真正的并发,所以jdk并没有采用这种方式取而代之的是两把不同的锁分离了put和take的操作,下面看源码:
1 | /** Lock held by take, poll, etc */ |
锁粗化
凡事都有一个度,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化.
1 | public void demoMethod() { |
上述代码会被整合成
1 | public void demoMethod() { |
在开发过程中,大家也应该有意识地在合理地场合进行锁的粗化,尤其当在循环内请求锁时.
1 | for (int i = 0; i < CIRCLE; i++) { |
一种更加合理地做法应该是:
1 | synchronized (lock){ |
注意: 性能优化就是根据运行时的真实情况对各个资源点进行权衡折中的过程,锁粗话的思想和减少锁持有时间是相反的,但是在不同的场合,他们的效果并不相同,所以大家要根据实际情况,进行权衡