对象在内存中的布局
synchronized实现的锁是存储在Java对象头里,什么是对象头呢?在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
Mark word记录了对象和锁有关的信息,当某个对象被synchronized关键字当成同步锁时,那么围绕这个锁的一系列操作都和Mark word有关系。Mark Word在32位虚拟机的长度是32bit、在64位虚拟机的长度是64bit。
锁的升级
在JDK1.6之前,synchronized是一个重量级锁,性能比较差。从JDK1.6开始,为了减少获得锁和释放锁带来的性能消耗,synchronized进行了优化,引入了 偏向锁和 轻量级锁的概念。所以从JDK1.6开始,锁一共会有四种状态,锁的状态根据竞争激烈程度从低到高分别是:无锁状态->偏向锁状态->轻量级锁状态(CAS,自旋锁)->重量级锁状态。这几个状态会随着锁竞争的情况逐步升级。为了提高获得锁和释放锁的效率,锁可以升级但是不能降级。
下面就详细讲解synchronized的三种锁的状态及升级原理
偏向锁
在大多数的情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作。偏向锁的获取过程非常简单,当一个线程访问同步块获取锁时,会在对象头中存储偏向锁的线程ID,表示哪个线程获得了偏向锁.偏向锁可以通过 -XX:+UseBiasedLocking开启或者关闭。
偏向锁的撤销
当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,撤销偏向锁的过程需要等待一个全局安全点(所有工作线程都停止字节码的执行)。
首先,暂停拥有偏向锁的线程,然后检查偏向锁的线程是否为存活状态
如果线程已经死了,直接把对象头设置为无锁状态
如果还活着,当达到全局安全点时获得偏向锁的线程会被挂起,接着偏向锁升级为轻量级锁,然后唤醒被阻塞在全局安全点的线程继续往下执行同步代码
轻量级锁加锁
JVM会先在当前线程的栈帧中创建用于存储锁记录的空间(LockRecord),然后将对象头中的Mark Word复制到该空间
线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
如果替换成功,表示当前线程获得轻量级锁,如果失败,表示存在其他线程竞争锁,那么当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁
CAS:表示自旋锁,由于线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说性能开销很大。同时,很多对象锁的锁定状态指会持续很短的时间,因此引入了自旋锁,所谓自旋就是一个无意义的死循环,在循环体内不断的重行竞争锁。当然,自旋的次数会有限制,超出指定的限制会升级到阻塞锁。
重量级锁
重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁),Mutex变量的值为1,表示互斥锁空闲,这个时候某个线程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被其他线程获得,其他线程调用lock只能挂起等待
为什么重量级锁的开销比较大呢?
原因是当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现,也就是相当于从用户态转化到内核态,而转化状态是需要消耗时间的