synchronize早已经没那么笨重

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

JVM锁优化

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

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

重量级锁

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

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

自旋锁

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

何谓自旋锁?并发

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

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

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

若是经过参数-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

相关文章
相关标签/搜索