探索synchronized
特性
- 原子性
- 可见性
- 有序性:同一个时刻只有一个线程访问同步代码块,即线程执行同步代码块是有先后顺序的
- 可重入性:避免了死锁
- 不可中断:一旦这个锁被其他的线程拥有,如果当前线程还想获得,只能等待或者阻塞(不会自己放弃),直到其他线程释放该锁
上锁的内容
对象锁(方法锁+同步代码块锁)
- 方法锁:synchronized修饰普通方法,锁对象默认为this
- 代码块:手动指定锁的对象
类锁
- synchronized修饰静态的方法或指定锁为Class对象
结构
指令码
// 进入
monditorenter
// 释放锁
monditorexit
底层实现
image.png对象头是synchronized实现锁的基础。synchronized申请锁,上锁,释放锁都与对象头有关
对象头的主要结构:<u>Mark Word</u> 和 <u>Class Metadata Address</u>
其中Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。(常常会与semaphore对比)
ObjectMonitor() {
_header = NULL;
_count = 0; //锁计数器(可重入原理)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
管程(synchronized基于linux的管程)
image.pngsynchronized基于管程模型。同步共享变量和入口等待队列来实现互斥,通过条件变量与条件变量等待队列实现通信。多线程竞争时,拿不到对象头monitor锁的线程便会被放到入口等待队列中。而获取到对象头monitor锁的线程则会进入临界区。此时同步代码块中存在条件变量判断是否进入wait方法时,满足条件变量则会继续执行,不满足条件变量的线程会被封装成ObjectWaiter放入条件等待队列中。待满足条件遍历的线程执行完毕时,会选择一个条件变量等待队列中的线程出队,唤醒线程进入临界区,与其他临界区的线程(入口等待队列)竞争
JVM对synchronized的优化
锁膨胀
锁膨胀升级的方向:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
偏向锁
一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word
的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word
的锁标记位为偏向锁以及当前线程ID等于Mark Word
的ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。
假如此时持有线程的锁没有释放,第二个线程便会在自旋等待(自旋锁)
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
自旋锁和自适应自旋锁(轻量级锁层面)
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。