首先进行结论总结
偏向锁: 偏向锁认为,获取锁的总是同一个线程
轻量级锁:轻量级锁认为,大多数情况下不会出现锁竞争,即使出现了锁竞争,获取锁的线程也能很快释放锁。获取不到锁的线程可以通过自旋等待一段时间,不会陷入阻塞状态。
偏向锁的流程
在锁对象的对象头中有一个ThreadId字段,如果字段是空,第一次获取锁的时候就把自身的ThreadId写入到锁的ThreadId字段内,把锁内的是否是偏向锁状态位置设置为1。下次获取锁的时候,直接查看ThreadId是否和自身线程Id一致,如果一致就认为当前线程已经取得了锁无需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段,提高了效率。
轻量级锁流程
线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并把对象头中的Mark Word复制到锁记录空间中,官方称为Displaced Mark Word,然后线程尝试持有CAS把对象头中的Mark Word替换成指向锁记录的指针,如果成功,当前线程获得锁,如果失败表示有其他线程竞争锁,当前线程尝试使用自旋来获取锁,获取失败就升级成重量级锁。
synchronized内部实现原理
synchronized关键字在应用层的语义是可以把任何一个非null对象作为锁,当synchronized作用在方法上时,锁住的是对象实例(this),作用在静态方法上锁住的就是对象对应的Classs实例,由于Class实例存在于永久代,因此静态方法锁相当于类的一个全局锁,当synchronized作用在一个对象实例上,锁住的就是一个代码块
ps:在HotSpot JVM中 monitor被称作对象监视器
ps : 如果学过一些JVM内存 其实方法区数据 也是放在永久代里面 非运行用数据都在堆
当有多个线程同时请求某个对象监视器时,对象监视器会设置几种状态来区分请求的线程:
Contention List:所有请求锁的线程被首先放置在该竞争队列中
Entry List:Contention List 中有机会获得锁的线程被放置到Entry List
Wait Set:调用wait()方法被阻塞的线程被放置到Wait Set中
OnDeck:任何一个时候只能有一个线程竞争锁 该线程称作OnDeck
Owner:获得锁的线程成为Owner
!Owner:释放锁的线程
对象头
java对象的内存布局包括对象头,数据和填充数据
数组类型的对象头使用3个字宽存储,非数组类型使用2个字宽存储,一个字宽等于四字节(32位)
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位
在运行期间随着锁标志位的变化存储的数据也会变化
偏向锁的锁标记位和无锁是一样的,都是01,但是有单独一位偏向标记设置是否偏向锁。
轻量级锁00,重量级锁10,GC标记11,无锁 01.
1.自旋锁 Spin Lock
处于Contention List,Entry List和Wait Set中的线程均属于阻塞状态,阻塞操作由操作系统完成(在Linux系统下通过pthread_mutex_lock函数),线程被阻塞后进入内核调度状态,这个会导致在用户态和内核态之间来回切换,严重影响锁的性能。
解决上述问题的方法就是自旋,原理是:
当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等等(自旋),在Owner线程释放锁之后,争用线程可能会立即获得锁,避免了系统阻塞
但是Owner运行的时间可能会超出临界值,争用线程自旋一段时间无法获得锁的话会停止自旋进入阻塞状态。
因此自旋锁对于执行时间很短的代码块有性能提高。
线程自旋的时候可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。
因此自旋的时间很重要,如果过长会影响整体性能,过短达不到延迟阻塞的目的。HotSpot认为最佳的时间是一个线程上下文切换的时间,但是目前只实现了通过汇编暂停集合CPU周期。
其他自旋锁的优化:
- 如果平均负载小于CPU的个数则一直自旋
- 如果超过CPU个数一半个线程正在自旋,则后面的线程会直接阻塞
- 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
- 如果CPU处于节点模式就停止自旋
- 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
- 自旋时会适当放弃线程优先级之间的差异
- 那synchronized实现何时使用了自旋锁?
答案是在线程进入ContentionList时,也即第一步操作前。
线程在进入等待队列时首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。
还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个
2.偏向锁(Biased Lock)
主要解决无竞争下的锁性能问题
按照之前HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如等待队列的CAS操作)。CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象偏向这个线程,之后的多次调用可以避免CAS操作。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,把锁恢复到标准的轻量级锁。
流程是这样的 偏向锁->轻量级锁->重量级锁
简单的加锁机制:
每个锁都关联一个请求计数器和一个占有她的线程,当请求计数器为0时,这个锁可以被认为是unheld的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁是,请求计数器就会加一,当线程退出synchronized块时,计数器减一。当计数器为0时,释放锁。
偏向锁的流程
在锁对象的对象头中有一个ThreadId字段,如果字段是空,第一次获取锁的时候就把自身的ThreadId写入到锁的ThreadId字段内,把锁内的是否是偏向锁状态位置设置为1。下次获取锁的时候,直接查看ThreadId是否和自身线程Id一致,如果一致就认为当前线程已经取得了锁无需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段,提高了效率。
但是偏向锁也有一个问题,就是当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态
对于偏向锁的抢占问题,一旦偏向锁冲突,双方都会升级会轻量级锁。之后就会进入轻量级的锁状态
偏向锁使用的是一种等到竞争出现才释放锁的机制,所以在其他线程尝试获取竞争偏向锁时,持有偏向锁的线程才会释放锁,释放锁需要等到全局安全点(在该时间点上没有字节码在执行)
消除偏向锁的过程是:
先暂停偏向锁的线程,尝试直接切换,如果不成功,就继续运行,并且标记对象不适合偏向锁,锁升级成轻量级锁。
关闭偏向锁:
偏向锁在jdk6和7中是默认开启的,但是总是在程序启动几秒钟后才激活
可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay=0
同时也可以使用参数来关闭偏向锁-XX:-UseBiasedLocking=false
轻量级锁
加锁流程:
线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并把对象头中的Mark Word复制到锁记录空间中,官方称为Displaced Mark Word,然后线程尝试持有CAS把对象头中的Mark Word替换成指向锁记录的指针,如果成功,当前线程获得锁,如果失败表示有其他线程竞争锁,当前线程尝试使用自旋来获取锁,获取失败就升级成重量级锁。
解锁流程:
会使用CAS操作来把Displaced Mark Word替换回对象头,如果成功表示没有竞争发生,如果失败,升级成重量级锁。
锁的优缺点比较
偏向锁:
优点:加锁解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距
缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗
适用场景:适合只有一个线程访问同步快的场景
轻量级锁:
优点:竞争的线程不会阻塞,提高程序的响应速度
缺点:如果始终得不到锁竞争的线程使用自旋会消耗CPU
适用场景:追求响应时间 同步块执行速度非常快
重量级锁:
优点:线程竞争不适用自旋 不会消耗CPU
缺点:线程阻塞 响应时间缓慢
适用场景:追求吞吐量 同步块执行时间长
ps:偏向锁和轻量级锁理念上的区别:
轻量级锁:在无竞争的情况下使用CAS操作去消除同步使用的互斥量
偏向锁:在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了
在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、
偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
锁粗化(Lock Coarsening) 减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁
锁消除(Lock Elimination) 通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步快以外被其他线程共享的数据的锁的保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(减少堆上上GC开销)
偏向锁(Biased Locking) 是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟
轻量级锁 (Lightweight Locking) 这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒
适应性自旋(Adaptive Spinning) 当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁
(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁),进入到阻塞状态。
对于synchronized加锁的完整过程描述:
检查Mark Word里存放的是否是自身的ThreadId,如果是,表示当前线程处于偏向锁,无需加锁就可获取临界资源
如果不是自身的ThreadId,锁升级,使用CAS来进行切换,新的线程根据MarkWord里现有的ThreadId,通知之前线程暂停,之前线程把MarkWord的内容设置为空
两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作把共享对象的MarkWord的内容修改为自己新建的记录空间的地址的方式竞争MarkWord
成功执行CAS的获得资源,失败的进入自旋
自旋在线程在自旋过程中,成功获得资源则整个状态依然处于轻量级的锁状态
如果自旋失败进入重量级锁的状态,自旋的线程进行阻塞,等待之前的线程完成并唤醒自己