使用方式:
- 方法锁:锁对象为调用者本身。
- 代码块加锁:锁对象为synchronized(Object o)传入的对象。
实现原理:
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
线程发生竞争时,只有一个线程会竞争锁成功,其余的会先进入到_cxq中,它是一种栈结构,先进后出。这即是synchronized非公平锁原理。当获取锁的线程调用wait方法时,此时锁会升级为重量级锁,线程会进入了monitor的_waitSet等待池中,当调用notify或者获取锁的线程执行结束需要唤醒其他线程时,进入_waitset中的线程会根据策略进入_cxq或_EntryList中,默认进入EntryList中。当下一次争抢锁的使用权时,会优先分配给_EntryList中的线程。如果_EntryList为空,则将_cxq中的线程按顺序放入_EntryList中并给第一个线程赋予锁的使用权。
MarkWord的结构
特点:
- 无锁态和偏向锁状态的锁标志位都是01,所以有"是否偏向锁"的字段来区别。
- 偏向锁没有地方存储hashcode,轻量级锁的hashcode存在栈帧的锁记录中,重量级锁会在 Monitor中记录 hashCode。所以偏向锁在未锁定状态调用hashcode会降级为无锁。偏向锁在锁定状态调用hashcode方法会升级为轻量级锁,轻量级锁会复制无锁状态的锁记录到栈帧中,以此来记录hashcode。
Synchronized的四种状态
- 无锁状态:锁对象在禁用偏向锁被创建出来未被任何线程所获取或者偏向锁撤销后的状态。
- 偏向锁:当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。锁初次被线程获取也是偏向锁模式。在获取锁的过程中,线程在锁对象的markword中记录偏向锁状态和当前线程的线程ID,当该线程再次请求锁时,只需检查锁对象的markword的锁标记是否为偏向锁和记录的线程ID是否为当前线程的ID,无需再做有关申请锁的操作,连CAS都不需要,所以性能较高。偏向锁使用了一种等到出现竞争才会释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁释放时,会将持有偏向锁的线程运行到安全点,然后检查该线程是否存活以及是否需要继续持有该偏向锁,如果线程存活或需要继续持有该偏向锁,则偏向锁升级为轻量级锁。如果线程不存活或不需要继续持有该偏向锁,则会重置锁状态为无锁状态,无锁状态只能升级为轻量级锁或重量级锁,无法再回到偏向锁状态。偏向锁在jdk1.6后默认开启,可以通过jvm参数来关闭偏向锁:-XX:-UseBiasedLocking = false。
//关闭延迟开启偏向锁
‐XX:BiasedLockingStartupDelay=0
//禁止偏向锁
‐XX:‐UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking
- 轻量级锁:线程在获取轻量级锁时会在栈帧中创建一块用于存储锁记录的空间(Lock Record),然后将锁对象头中的markword(无锁状态的markword)复制到这块空间当中,称之为Displaced MarkWord(无家可归的markword)。之后用CAS机制尝试一次(不自旋)将锁对象头的中的markword替换为指向当前栈帧中锁记录的指针。这个操作如果成功,则当前线程获取到轻量级锁。在这个过程中,它还会检查当前锁状态是否已被自己获取,如果是则说明本次获取锁是一个重入操作。对于重入操作,同样的会在栈帧中创建一个区域用于存储锁记录,只是这个锁记录里的markword是空,释放锁时遵循后进先出的原则释放锁。在最后一个Lock Record被释放时将无锁状态的markword放进锁对象头中,锁被重置为无锁状态;如果获取锁不成功,则锁升级为重量级锁。
即轻量级锁所适应的场景是线程交替执行同步块的场合 ,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
- 重量级锁:锁在膨胀成重量级锁的期间会创建一个monitor对象。即重量级锁的原理是monitor机制。重量级锁膨胀的过程分为两步,第一步:获取monitor锁对象。这个过程是通过for循环一直重复进行,首先检查monitor锁是否已存在,如果存在直接返回。如果不存在,则判断锁是否在膨胀中,如果是则让出CPU,如此循环16次,如果仍然在膨胀,则挂起线程。如果没有在膨胀中,如果当前是轻量级锁,则创建monitor对象。第二步,调用monitor的entry方法获取锁。这个过程中通过CAS将markword指向monitor对象,同时将minitor的owner指针指向当前线程,这个过程涉及到多次的自适应自旋获取锁,如果这个过程成功,则锁升级为重量级锁,如果不成功,最终迫不得已而挂起线程。重量级锁的竞争会导致线程在用户态和内核态来回切换,性能急剧下降,除了拥有锁的线程外,其他线程都会阻塞。适用于追求吞吐量,同步块或同步方法执行时间较长的场景。
偏向锁撤销之调用对象HashCode
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
- 当对象可偏向但未偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
- 当对象正处于已偏向时,调用HashCode将使偏向锁强制升级成重量锁。
偏向锁撤销之调用wait/notify
假设有两个线程, A先获取到的就是偏向锁,那么B就会通过CAS尝试锁的获取。当B获取到锁时,会检查A是否还存活,如果不,则锁被重置为无锁状态,此时B竞争到锁,将其设置为轻量级锁。如果存活,则锁升级为轻量级锁。当BCAS失败没获取到锁时,锁就自动升级为重量级锁。重量级锁释放后转为无锁状态,monitor被当作垃圾回收。第二种场景,如果有N个线程同时竞争锁,其中只有一个自旋失败没有获取到锁,那么锁也会升级成重量级锁。
synchronized锁状态变化总结
- 锁对象创建时可能有两种状态,如果禁用了偏向锁,或者开启了偏向锁但在服务启动的前4秒内,此时创建的对象是无锁状态,反之,则是匿名可偏向状态。
- 锁处于匿名可偏向状态时,如果调用hashcode方法则降级为无锁状态;如果此时有线程获取到了匿名可偏向锁,则锁转为偏向锁已偏向状态。
- 锁处于已偏向状态时,即使当前线程释放了锁,锁仍然会处于已偏向状态,不会转为无锁状态,直到出现了另一个线程的锁竞争,才会导致锁状态的转变。
- 偏向锁已偏向时转变锁有三种情况,第一,当出现了线程的竞争时,如果当前线程已经释放了获取的偏向锁,则锁会先重置为无锁状态,然后升级为轻量级锁。第二,如果出现线程竞争时,当前线程仍然没释放锁,则锁直接升级为轻量级锁。第三,即使未出现锁竞争,但当前线程在获取锁的时间段内,执行了hashcode或wait方法,则直接升级为重量级锁。
- 线程一旦转为无锁状态,则永远无法再转为偏向锁状态。在无锁状态时,如果出现轻微锁竞争,即只有一个线程去竞争,且竞争成功了,则升级为轻量级锁。如果出现大量竞争,即有一部分线程直接竞争失败了,则直接升级为重量级锁。
- 线程在竞争轻量级锁失败时,会导致锁升级为重量级锁。无锁、轻量级锁、重量级锁都无法再转为偏向锁。轻量级锁、重量级锁释放后都会转为无锁状态。
- 重量级锁依赖于monitor机制实现,会导致用户再内核态和用户态之间切换,性能很差。
偏向锁批量重偏向&批量撤销
在开启偏向锁的条件下,对象创建出来则是匿名可偏向状态。如果此时线程竞争频繁时,偏向锁升级会先撤销为无锁再升级为轻量级锁。在这种情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
批量重偏向:当一个类的所有对象锁累计撤销超过20次,即同一个类的多个对象锁累计由偏向锁转为轻量级锁超过20次,则jvm会认为之前的冲偏向策略可能时存在问题的,它会将20次之后的锁升级操作改为再次重偏向的操作偏向新的线程,即20次之前的锁升级为轻量级锁,20次之后的锁仍然是偏向锁。
批量撤销: 当一个类的所有对象锁累计撤销超过40次,则jvm会认定这个类不适合使用偏向锁,之后,对于该class的锁,直接走轻量级锁的逻辑。即新对象创建出来为无锁状态,加锁直接加轻量级锁。
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值
自旋优化
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
- Java 7 之后不能控制是否开启自旋功能
锁粗化
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}