Java高并发锁优化——一点建议和java虚拟机所做的努力
仅作为笔记
文章目录
前言
仅作为笔记
一、有助于提高“锁”性能的几点建议
1.1、减小锁持有时间
- 核心思想是对于一个方法,其中可能需要同步的步骤在整个方法的时间占比很少,这样的情况下如果对整个方法加锁会导致锁的使用时间过长,容易使得执行这个方法的线程等待过长时间,产生不必要的浪费。
- 使用同步代码块的方式只让部分步骤同步,这样减少了线程对锁的持有时间,有利于减少阻塞时间和概率。核心还是有助于降低锁冲突的可能性。
1.2、减小锁粒度
- 减小锁粒度简单的来说就是大锁换多把小锁的思想,把大的问题拆分开。
- 比如ConcurrentHashMap相对于使用整个加锁的HashMap来说就降低了锁粒度,CouncurrentHashMap内部分为多个小的HashMap,叫做段(segment),一般是16个。这样在高并发插入新的值得时候,只要每个线程插入的值不在一个段中,那么这些线程就不存在冲突的问题。
- 可以拆分是有条件的,也不是任何地方都可以拆分,拆分的前提是涉及的变量或者说是数据是相互不关联的。
- 锁粒度减小了也不见得在任何场合都是好事,在必须涉及全局的时候,这就是一个负担,因为这必须先获得所有的小锁才能称得上是对全局有了掌控,而获得锁这个操作是有开销的。所以这样的情况下减小了锁粒度成了负担。
总结:减小锁粒度就是在数据结构角度对数据进行切割,缩小锁定对象的范围,从而减少锁冲突的可能性,进而提升系统的并发能力。
关于ConcurrentHashMap的详细解析请点击这里
1.3、读写分离锁替换独占锁
读写分离锁就是之前讲的应用于读操作多于写操作的锁,是在系统功能的角度进行的优化操作,与之相对的就是独占锁。
1.4、锁分离
- 最经典的应用就是前面讲的链表阻塞队列(LinkedBlockingQueue),前面提到过其take()和put()操作是使用的分离锁,因为对于链表队列来说,其take和put操作的是队列的头和尾,在数据结构层面来看是不存在冲突的,而在并发环境下如果两个线程要操作他们,在逻辑上是不存在冲突的,但是如果使用同一把锁就会使得另一个操作必须等待,在竞争激励得环境下是个非常不明智的浪费。所以对这两个操作使用两把不同的锁,这个思想就是分离锁。
1.5、锁粗化
- 把一连串得连续得对同一把锁得不断请求和释放得操作合成对锁的一次请求,这样会规避了给那些不需要同步的操作加锁,从而减少对锁的请求同步次数。
- 打个比方,一个班的学生都需要去一个只属于他们班级的实验室(所以别的不能进)拿他们自己的东西和看一看实验室黑板的通知。拿东西是一个需要同步的操作,防止重复拿和拿错,但是看看黑板不需要同步啊,这时候如果让学生拿东西加一把锁,看黑板也要加一把锁是非常浪费时间的行为,此时让其拿了钥匙就进去拿东西顺便瞟一眼黑板反而是节约时间的。如下:
总之:当把发现不需要同步的工作剔除需要同步的部分这个带来的锁粒度减小反而代价比不分开大时,还不如加大锁粒度。
注意:同步的步骤包括:锁请求、同步、释放,这些步骤都是需要消耗CPU资源的。
二、Java虚拟机对锁优化所做的努力
2.1、偏向锁
应用于线程竞争不激烈的情况下,如果一个线程在竞争不激烈的情况下拿到了锁,下一次他就不用再获取锁了。打个比方,还是你们班需要去拿东西,可今天是周末,就你一个人要去拿东西,拿几趟,你只需要开了一次门就行了,不用拿一次开一次,这样浪费时间,也没人和你争着进去,你的同学没在。当然,这只用于竞争不激烈的情况。
2.2、轻量级锁
首先要明白两个东西:
- 加锁:(1)JVM在当前线程栈帧中创建用于存储锁记录的空间;(2)将对象头中的Mark Word复制到锁记录中,称为Displaced Mark Word;(3)线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则代表获得锁,失败表示其他线程竞争锁,当前线程尝试使用自旋(CAS总是搭配自旋)操作来获取锁。
- 解锁:(1)使用CAS将Displaced Mark Word替换回到对象头;(2)如果成功,则表示没有竞争发生;(3)如果失败,则表示当前锁存在竞争,锁就会膨胀为重量级锁。
显然,加锁和解锁是需要消耗不少资源的,而轻量级锁是在竞争不那么激烈的情况下,简化传统的重量级锁使用以上方法的步骤实现快速加锁解锁,方法是以对象头为指针指向持有锁的线程堆栈内部,判断其是否持有锁。如果轻量级锁加锁失败,那么当前线程锁的请求会膨胀为重量级锁。
2.3、自旋锁
发生在锁膨胀以后,虚拟机为了避免这个线程真的被操作系统挂起,会让他在原地空循环几次,相信他可能会在不久得到锁进入临界区。如果空循环完了还是不能得到锁,这时才把线程在操作系统层面挂起,经常搭配CAS。
2.4、锁消除
就是在即时编译器(JIT)编译时,通过上下文扫描去除不可能存在的共享资源竞争的锁。关键实现计数是逃逸分析。也就是观察某一个变量是否会逃出某一个作用域以外,没有的话就可以清除这上面加的无意义的锁。
如下Vector的使用:
2.5 逃逸分析
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
例如以下代码:
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。
使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。