我发现一些同学在网络上有看不少synchronize的文章,可能有些同学没深入了解,只看了部分内容,就急急忙忙认为不能使用它,很笨重,因为是采用操作系统同步互斥信号量来实现的。关于这类的对于synchronize的污点,我打算帮它清洗下。

JVM锁优化

其实jdk1.6对锁的实现已经引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

重量级锁

重量级锁,是JDK1.6之前,内置锁的实现方式。简单来说,重量级锁就是采用互斥量来控制对互斥资源的访问。

历史回顾:在JDK1.6以前的版本,synchronized实现的内置锁都比较重(这也是诸多同学们理解的版本)。JVM中monitorentermonitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

何谓自旋锁?

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;

如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。

锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

偏向锁

既然采用了内置锁,只要访问了同步代码,都会涉及获取锁和释放锁的动作。而这种动作都是存在开销的。无论是重量级锁去取得互斥信号量,还是轻量级锁去compare,都会有开销。然后很多时候,被内置锁约束的同步代码段往往只有一个线程去获取“锁”,根本不存在并发访问。那么这时候频繁地加锁和解锁就会有额外的开销。因此偏向锁也应运而生。

在采用偏向锁时,如果一个线程第一次来访问互斥资源,则在对象头和栈帧的锁记录中存储偏向锁的线程ID(可以理解为获取“锁”的动作)。偏向锁在获取锁之后,直到有竞争出现才会释放锁。也就是说,如果长期没有竞争,偏向锁是一直持有锁的。这样,当线程下次再次进入同步块的时候不需要进行任何获取锁的操作,即可访问互斥资源。节约了频繁获取锁和释放锁的开销。

轻量级锁

轻量级锁,顾名思义,相比重量级锁,其加锁和解锁的开销会小很多。重量级锁之所以开销大,关键是其存在线程上下文切换的开销。而轻量级锁通过JAVA中CAS的实现方式,避免了这种上下文切换的开销。当compare失败的时候(理解成没有拿到”锁”),线程不会被挂起;当compare成功的时候,可以直接对互斥资源进行修改(就好像拿到了“锁一样”)。重量级锁使用互斥信号量实现,如果没有拿到互斥信号量(理解成没有拿到“锁”),线程会被挂起;如果拿到互斥信号量则可以直接对互斥资源进行访问。

从以上分析可知,其实是否拿到“锁”对于不同的锁实现方式有着不同的含义。 重量级锁基于互斥信号量实现,则认为拿到互斥信号量即为拿到锁。而CAS操作则通过compare是否成功来判断是否拿到“锁”。 这里的“锁”都不是特指某一具体事物,而是一种“条件”,拿到了“锁”,即意味着满足了“条件”,可以对互斥资源进行访问。当然本质上,无论哪种实现方式,拿到锁之后都会去修改Mark Word,来记录自己确实拿到了锁;释放锁则会清空Mark word中自己的线程ID。

轻量级锁和重量级锁的重要区别是: 拿不到“锁”时,是否有线程调度和上下文切换的开销。

轻量级锁加锁:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间(Lock
Record),并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark
Word。然后线程尝试使用CAS将对象头中的Mark
Word替换为指向锁记录的指针。如果成功,当前线程获得锁。如果这个更新操作失败了,虚拟机首先会检查
对象的Mark Word是否指向当前线程的栈帧。如果指向,说明当前线程已经拥有了这个对象的锁,那就可以直
接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁
,那轻量级锁就不再有效,要膨胀为重量级锁,Mark Word中存储的就是指向重量级(互斥量)的指针
复制代码

轻量级锁解锁:

轻量级解锁时,会使用原子的CAS操作来将Displaced Mark
Word替换回到对象头,如果成功,则整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该
锁,那么其他线程就要在释放锁的同时,唤醒被挂起的线程。
复制代码

关于轻量级锁的加锁和解锁过程简单来说就是:

  • 尝试CAS修改mark word:如果这步能直接成功,则代价较小,可以直接获取锁
  • 获取锁失败则采用自旋锁来获取锁(CAS修改尝试失败后采取的策略)
  • 自旋锁尝试失败,锁膨胀,成为重量级锁:自旋锁也尝试失败,不得不使用重量级锁,线程也被阻塞。

总结

所以synchronize并有没像之前想象的那么笨重,其实大家可以在大量的源码中都能看到它的身影,包括juc包下的工具类等等,总之存在必有合理之处,望大家善用它。(当然前提必须理解它)

PS:一个好消息

同学,你造吗?阿里云和腾讯云已白菜价,云服务器低至不到300元/年。这里有一份云计算优惠活动列表,来不及解释了,赶紧上车!


转自https://juejin.im/post/5bff854b5188250e8601ec90

 

synchronize早已经没那么笨重的更多相关文章

  1. java提升路线书单(原文自知乎刘欣)

    复制黏贴自知乎刘欣大神,作为个人的书单与指导路线 原文链接:https://www.zhihu.com/question/19848946/answer/92536822   刘欣 追寻内心的真正兴趣 ...

  2. [gist]在浏览器里免查看源代码格式化var_dump输出

    Gist Link /** * 格式化var_dump输出... * 我勒个去..早怎么没想到..就加了个pre啊,, */ function var_dump_html($var){ echo &q ...

  3. First day in 阿里

    周五上午10点半的飞机,为了便宜选了CA的空客320的飞机,结果体验很差.飞机涂了层风骚的粉紫色,机内较旧,也很小,经过所谓的头等舱简直惨不忍睹.对比起去年飞去北京乘的波音真是没法比,波音上每个人都有 ...

  4. JavaScript宝座:七大框架论剑

    JavaScript宝座:七大框架论剑 一周前,Throne of JS大会在多伦多召开,这应该是我参加过的最有料也最不一样的一次大会.大会官网如是说: 加载整个页面,然后再“渐进增强”以添加动态行为 ...

  5. DWZ使用笔记

    DWZ使用笔记 一.前言     在最近的一个项目中,引入了DWZ这个富客户端框架,算是一次尝试吧.期间也遇到不少问题,总算一一解决了.特以此文记之.     本人用的是dwz-ria-1.4.5+A ...

  6. CF Anya and Ghosts (贪心)

    Anya and Ghosts time limit per test 2 seconds memory limit per test 256 megabytes input standard inp ...

  7. 请列出你在从事IT生涯中,最难以忘怀的一次误操作

    IT系统最怕什么,我觉得就两点: 1.不可靠的软硬件. 2.误操作. 第一点就不用解释了,第二点是该文的内容,主要摘选自ITPUB的精华贴——[精华] 请列出你在从事DBA生涯中,最难以忘怀的一次误操 ...

  8. SVN 资源库报错 E175002

    遇到一个问题, svn: E175002: OPTIONS request failed on '/' Connection timed out: connect 试了网上好多办法,都没有,最后公司大 ...

  9. 假如时光倒流,我会这样学习Java

    回头看看, 我进入Java 领域已经快15个年头了, 虽然学的也一般, 但是分享下我的心得,估计也能帮大家少走点弯路. [入门] 我在2001年之前是C/C++阵营, 有C和面向对象的基础, 后来转到 ...

随机推荐

  1. Do not throw System.Exception, System.SystemException, System.NullReferenceException, or System.IndexOutOfRangeException intentionally from your own source code

    sonarqube的扫描结果提示 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/exceptions/creatin ...

  2. C++数组作为函数参数的几个问题(转)

    本文需要解决C++中关于数组的2个问题:1. 数组作为函数参数,传值还是传址?2. 函数参数中的数组元素个数能否确定? 先看下面的代码. #include <iostream> using ...

  3. Java中的ArrayList

    ArrayList是所谓的动态数组.用一个小例子: import java.util.ArrayList; import java.util.Iterator; import java.util.Li ...

  4. SPOJ:Elegant Permuted Sum(贪心)

    Special Thanks: Jane Alam Jan*At moment in University of Texas at San Antonio - USA You will be give ...

  5. 【Sdoi2008】沙拉公主的困惑

    [题目链接] 点击打开链接 [算法] gcd(a,b)=gcd(a mod b, b),又m!|n!          则有ans=(n!/m!)·ϕ(m!)          由ϕ(n)=n(1-1 ...

  6. day4装饰器-迭代器&&生成器

    一.装饰器 定义:本质是函数,(装饰其他函数)就是为其它函数添加附加功能 原则:1.不能修改被装饰的函数的源代码 2.不能修改被装饰的函数的调用方式 实现装饰器知识储备: 1.函数及“变量” 2.高阶 ...

  7. linux设备驱动第三篇:如何实现一个简单的字符设备驱动

    在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动.本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存 ...

  8. 是时候开刷NOI了

    整天挨着毛爷爷,压力好大.. 看毛爷爷即将炖完NOI,我的确也该刷了 原则是从头到尾自己想(虽然看了一次题解),可以不A掉. NOI2009 day1: T1 题目略神,我还是不讲了...(就这题我W ...

  9. opencord视频截图

    参考:https://www.youtube.com/watch?v=Teu9jK6GF6s

  10. P4692 [Ynoi2016]谁的梦

    传送门 分别考虑每一种颜色对答案的贡献.每种颜色的贡献就是他出现的区间个数,那么可以用总区间减去不包含它的区间个数,把每一个序列里不包含它的区间个数加起来,然后不同序列用乘法原理计算即可 于是我辛辛苦 ...