Java 锁粗化和锁消除
原文:Java 锁消除和锁粗化
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,即使单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
一种极端的情况如下:
public void doSomethingMethod(){ synchronized(lock){ // do some thing } // 这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕 synchronized(lock){ // do other thing } }
上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下:
public void doSomethingMethod(){ // 进行锁粗化:整合成一次锁请求、同步、释放 synchronized(lock){ // do some thing // 做其它不需要同步但能很快执行完的工作 // do other thing } }
注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。
另一种需要锁粗化的极端的情况是:
for(int i = 0; i < size; i++){ synchronized(lock){ } }
上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在 JDK 内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。
锁粗化后的代码如下:
synchronized(lock){ for(int i = 0; i < size; i++){ } }
锁消除
锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。
比如,StringBuffer 类的 append 操作:
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
从源码中可以看出,append 方法用了 synchronized 关键词,它是线程安全的。但我们可能仅在线程内部把 StringBuffer 当作局部变量使用:
public class Demo { public static void main(String[] args) { long start = System.currentTimeMillis(); int size = 10000; for (int i = 0; i < size; i++) { createStringBuffer("Hello", "Word"); } long timeCost = System.currentTimeMillis() - start; System.out.println("createStringBuffer:" + timeCost + " ms"); } public static String createStringBuffer(String str1, String str2) { StringBuffer sBuf = new StringBuffer(); sBuf.append(str1); // append 方法是同步操作 sBuf.append(str2); return sBuf.toString(); } }
代码中 createStringBuffer 方法中的局部对象 sBuf,只在该方法内的作用域有效,不同线程同时调用 createStringBuffer() 方法时,都会创建不同的 sBuf 对象,因此此时的 append 操作若是使用同步操作,就是白白浪费的系统资源。
这时我们可以通过编译器将其优化,将锁消除,前提是 Java 必须运行在 server 模式(server 模式会比 client 模式作更多的优化),同时必须开启逃逸分析:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis
表示开启逃逸分析,+EliminateLocks
表示锁消除。
逃逸分析:比如上面的代码,它要看 sBuf 是否可能逃出它的作用域?如果将 sBuf 作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说 sBuf 这个对象发生逃逸了,因而不应将 append 操作的锁消除,但我们上面的代码没有发生对象逃逸,锁消除就可以带来一定的性能提升。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2022-07-01 使用 git rebase 合并多个 commit