Synchronized关键字本质上是通过使得对象与操作系统提供的一个monitor对象关联来实现对对象上锁。为什么称它为重量级锁,就是因为它的锁竞争是在操作系统的层面中进行的,因为线程获取锁的过程是通过moniter对象来进行竞争和持有的。
虽然它是最重量级的,但是它提供了最高级别的并发安全性,它能够同时保证:
1. 原子性:确保线程互斥的访问同步代码
2. 可见性:保证共享变量的修改能够及时可见
3. 有序性:禁止指令重排序
synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,。每一个被锁住的对象都会和一个monitor关联,(解决:每个对象都可以上锁的问题)同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
通过monitorenter获取锁、通过monitorexit释放锁。
底层利用了几个虚拟队列,新请求锁的线程将首先被加入到ContentionList中(通过CAS实现),ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为队头)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 Ready(OnDeck)线程,只是把竞争锁的权利交给Ready(OnDeck),Ready(OnDeck)线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。
OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不 发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。还要重新获取锁
那synchronized实现何时使用了自旋锁?答案是在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时 首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。
为什么要加入EntryList这个队列?我们可以从一个高并发的场景中去理解它
比如在高并发场景中,1万个线程竞争一个synchronized锁,只有一个锁得到了资源,会执行10ms后释放锁,没有获得锁的线程都会去自旋15次,而这10ms的时间足够每个线程去自旋15次,那么就耗费了大量的cpu资源去做无意义的自旋
所以需要优化
我们让线程去自旋竞争一个EntrySet,而不是一个10ms的Synchronized锁,我们竞争一个几纳秒的操作,即加入一个EntrySet链表,这样大多数线程就不用自旋那么多次很快就竞争到了加入EntrySet的权限,然后加入进去就休眠了,只让EntrySet另一端即一开始加入EntrySet的线程继续自旋竞争Synchronized锁,它是唯一一个用完15次自旋的线程去竞争10ms的Synchronized锁,如果竞争到就变成持有者,没竞争到就进入waitSet,只有waitset中的线程有被持有者执行完锁后去唤醒的权利,waitset中的线程数相比Entryset也不会太多,所以这样竞争线程数也不会太多,节省了大量的自旋竞争时间。
锁升级的过程
偏向锁 VS 轻量级锁 VS 重量级锁,是指锁的状态,专门针对synchronized的。
偏向锁
偏向锁是在线程第一次对对象上锁时,将线程ID写入到锁对象的mark word中,当发生锁重入时,只需检查obj的mark word中的线程ID是否是自己,是的话就无需其他操作。
偏向锁主要解决无竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题:
现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
首先JVM要设置为可用偏向锁。然后当一个进程访问同步块并且获得锁的时候,会在对象头和栈帧的锁记录里面储存取得偏向锁的线程ID。
下一次有线程尝试获取锁的时候,首先检查这个对象头的MarkWord是不是储存着这个线程的ID。如果是,那么直接进去而不需要任何别的操作。如果不是,那么分为两种情况。1、对象的偏向锁标志位为0(当前不是偏向锁),说明发生了竞争,已经膨胀为轻量级锁,这时使用CAS操作尝试获得锁。2、偏向锁标志位为1,说明还是偏向锁不过请求的线程不是原来那个了。这时只需要使用CAS尝试把对象头偏向锁从原来那个线程指向目前求锁的线程。这种情况举个例子就是老王准备退休了,他儿子接替他来拿钥匙,于是仓库管理员认识了他儿子,他儿子每次来也不用登记注册了。
轻量级锁(是指锁对象,如lock,obj)
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。
轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录(Lock Record)中,官方称为Displaced Mark Word。然后线程尝试使用 CAS 将对象头(锁的头)中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争(一旦有第二个线程竞争,变成重量级锁),除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
因为自旋会消耗 CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
锁升级
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
当第一个线程进入后,发现锁对象的对象头中的偏向锁位没有被设置,就直接获得执行临界区的权力,这就是偏向锁,对象头的设置偏向锁id位线程id
当第二个线程过来后发现偏向锁位被设置了,它就去自旋加入EntrySet,用到的是轻量级锁,然后再去竞争owner,失败后进入waitset,升级为重量级锁的竞争
偏向锁发现了竞争就升级为轻量级锁,轻量级锁拿不到持有者就升级为重量级锁
轻量级锁解锁时,会使用CAS将之前复制在栈桢中的 Displaced Mard Word 替换回 Mark Word 中。如果替换成功,则说明整个过程都成功执行,期间没有其他线程访问同步代码块。
但如果替换失败了,表示当前线程在执行同步代码块期间,有其他线程也在访问,当前锁资源是存在竞争的,那么锁将会膨胀成重量级锁。图三中重量级锁部分也就眼神了锁膨胀的过程。
没有竞争时,就是偏向级锁,发现了竞争,就升级为轻量级锁,轻量级锁自旋后拿不到持有者,就进去waitset,升级为重量级锁。