这里是目录标题
前言:
Synchronize实现实现同步最常用的方式,它的底层是如何实现的?
Synchronized会锁定什么?
Synchronized锁定的是对象,在不同地方使用会锁类对象或实例对象
- 实例方法:锁定实例对象
- 静态方法:锁定类对象
- 代码块
- synchronized(this) {}:锁定实例对象
- synchronized(class.object){} 锁定类对象
JDK1.6前Synchronized实现:
- 通过Synchronized修饰的代码块,线程通过获取Monitor对象,来实现获取锁。在同步代码块的开始会引入monitorenter,在结束的位置引入monitorexit,在访问这一对monitor的时候,线程必须要持有monitor对象,并将这个对象记录在自己的私有monitor record列表中,表示当前线程具有这个对象的锁。
每个对象只有一个与之对应的monitor
,也就是说monitor只能被一个线程所持有。(线程拥有自己monitor record,对象也有自己的monitor等待队列) - 可重入性:当前线程再次申请所拥有对象的锁时(在同步代码中再次执行被Synchronized修饰的方法),可以再次拿到这个对象的锁。
- 被Synchronized修饰的方法,是通过在字节码上添加ACC_Synchronized标志。JVM 通过该
ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,如果是实例⽅法,JVM 会尝试获取实例对象的锁。如果是静态⽅法,JVM 会尝试获取当前 class 的锁。 - Monitor监视器锁的内容结构
- Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
- EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程
- RcThis:表示等待在该monitor的所有线程的个数。
- Nest:用来对重入锁次数的计数。
JDK6以后做的优化
锁出现不同类型,无锁状态-偏向锁-轻量锁-重量锁(原Synchronized)
JVM是如何区分锁处于不同状态呢?需要一个位置记录锁出在什么状态,这时候就需要理解Object头
Java对象头(存储锁的类型)
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐填充。
对象头中包含两部分:
- MarkWord(存储锁的类型)
- 对象的类型指针
- 如果是数组对象的话,对象头还有一部分是存储数组的长度。
多线程下 synchronized 的加锁就是对同一个对象的对象头中的 MarkWord 中的变量进行CAS操作。
不同类型的锁下MarkWord 的内容
-
无锁状态
-
偏向锁
可以看到在偏向锁状态下,相比无锁状态多了线程ID和epoch,且是否是偏向锁变为了1.
注意:如果这个对象计算过hashcode的话,就无法使用偏向锁了(因为没有地方存储对象的hashcode),会发生锁升级。
偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
- 轻量锁、重量锁
轻量锁这里指向持有锁的线程,它内部的栈帧中的 Lock Record 记录。
重量锁这也就是指向1.6之前的Monitor监视器对象。
64位的JVM打印对象头信息
public static void biasedSyn() {
// 偏向锁在应用程序启动后的几秒钟内(默认延迟为4秒)才会激活,所以此处主线程休眠5秒之后,创建的对象默认启动偏向锁
ThreadUtil.sleepSeconds(5);
Object o = new Object();
System.out.println(o.hashCode());
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 17 03 (00000101 00111000 00010111 00000011) (51853317)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
mark word是反向存储的,根据mark word是由前64位的value倒序拼接成的串组成,所以Mark Word头部信息如下
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
倒数后三位101,表示当前对象处于偏向锁阶段。
锁的升级过程
1、当资源对象处于无锁状态(JDK1.6之后默认使用偏向锁,创建的对象是处于匿名偏向锁状态,注意偏向锁在应用程序启动后的几秒钟内(默认延迟为4秒)才会激活,所以5秒之后,创建的对象默认启动偏向锁)
通过synchronized加锁之前,对象未进行过hashcode —— 偏向锁
通过synchronized加锁之前,对象进行过hashcode —— 轻量锁
如何升级:通过使用 CAS 将 MarkWord 中的线程ID设置为自己的线程ID,就升级为偏心锁
2、当资源对象处于偏向锁状态
- 加锁
- 如果持有锁的线程再次申请锁,会直接获取锁(偏向的含义)
- 如何请求加锁的线程 ID 不是持有锁的线程 ID,需要进行锁升级(升级需要进行偏向锁的撤销)
- 撤销
- MarkWord 中指向的线程不存活,退回到可偏向但未偏向的状态
- MarkWord 中的线程存活
- 线程ID指向的线程仍然拥有锁,升级为轻量级锁,将 mark word 复制到线程栈中
- 不再拥有锁,退回到可偏向但未偏向的状态
3、当资源对象要变为轻量锁状态
- 加锁
- 线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向线程中锁记录(Lock Record)的地址。如果成功,当前线程获得轻量级锁,如果失败,虚拟机先检查当前对象头的 Mark Word 是否指向当前线程的栈帧,如果指向,则说明当前线程已经拥有这个对象的锁,则可以直接进入同步块 执行操作,否则表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数 达到界限值(threshold),轻量级锁将会膨胀为重量级锁。
- 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。
- 撤销
- 轻量级锁解锁时,如果对象的Mark Word仍然指向着线程的锁记录,会使用CAS操作, 将修改Mark Word,如果成功,则表示没有竞争发生。如果失败, 表示当前锁存在锁竞争,锁就会膨胀为重量级锁。
3、当资源对象要变为重量锁
- 重量级锁(heavy weight lock),是使用操作系统互斥量(mutex)来实现的传统锁。
- 当锁膨胀(inflate)为重量锁时,就不能再退回到轻量级锁。
synchronized 是非公平锁
在线程释放锁的时候,JVM会从等待队列中随机获取一个线程来获取锁。
优点是:因为减少了线程切换的开销,所以非公平锁通常具有更好的性能。
缺点是:可能产生饥饿现象
锁消除
锁消除(lock eliminate):虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
锁粗化(Lock coarsening)
将临近的代码块用同一个锁合并起来。