synchronized实现原理和锁优化

概述

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象

静态同步方法,锁是当前类的class对象

同步方法块,锁是括号里面的对象

同步代码块是使用monitorenter和monitorexit指令实现的

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现

对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

Java对象头

synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、class Pointer(类型指针)。其中class Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

Monitor

Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

自旋锁

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

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

适应自旋锁

JDK 1.6引入了自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果上一次自旋成功了,那么下次自旋的次数会更加多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

锁消除

在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。(我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法, JVM可以大胆地将vector内部的加锁操作消除)。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,减少加锁解锁的频率。

 

流程是这样的 偏向锁->轻量级锁->重量级锁

偏向锁

主要解决无竞争下的锁性能问题,每次加锁/解锁都会涉及到一些CAS操作(比如等待队列的CAS操作)。CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象偏向这个线程,之后的多次调用可以避免CAS操作。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,把锁恢复到标准的轻量级锁。

轻量锁

线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并把对象头中的Mark Word复制到锁记录空间中,官方称为Displaced Mark Word,然后线程尝试持用CAS把对象头中的Mark Word替换成指向锁记录的指针,如果成功,当前线程获得锁,如果失败表示有其他线程竞争锁,当前线程尝试使用自旋来获取锁,获取失败就升级成重量级锁。

总结

锁粗化(Lock Coarsening): 减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁

锁消除(Lock Elimination) :通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步快以外被其他线程共享的数据的锁的保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(减少堆上上GC开销)

偏向锁(Biased Locking) :是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟

轻量级锁 (Lightweight Locking) :这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒

适应性自旋(Adaptive Spinning) :当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁

(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁),进入到阻塞状态。

 

猜你喜欢

转载自blog.csdn.net/u012133048/article/details/85490843