多线程互斥多线程引入偏向锁和轻量级锁的原因?
synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有的线程挂起,让cpu通过操作系统指令,去调度多线程之间,谁执行代码块,谁进入阻塞状态。
这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,这样就会大量消耗资源,程序运行的效率低下。为了提高效率,jvm的开发人员,引入了偏向锁,和轻量级锁,尽量让多线程访问公共资源的时候,不进行程序运行状态的切换,由用户态进入内核态,借助操作系统进行互斥。
Jvm规范中可以看到synchronized在jvm里实现原理,jvm基于进入和退出Monitor对象来实现方法同步和代码块同的。在代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和异常结束处)织入monitorexit指令实现。
线程执行到monitorenter处,将会获取锁对象锁对应的monitor的所有权,即尝试获得对象的锁。(任意对象都又一个monitor与之关联,当且一个monitor被持有后,他处于锁定状态)
java的多线程安全是基于lock机制实现的,而lock的性能往往不如人意。原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是jvm依赖操作系统互斥(mutex - fast mutex = futex(可以减少内核态以及资源的切换))来实现的。互斥是一种会导致线程挂起,并在较短时间内又需要重新调度回原线程的,较为消耗资源的操作。
为了优化java的Lock机制,从java6开始引入轻量级锁的概念。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了cpu原语Compare-And-Swap(cas,汇编指令CMPXCHG/LOCK CMPXCHG),尝试进入互斥前,进行补救。
引入多种类型的锁的机制
在JDK1.6之前,synchonized同步方式的成本非常高,因为使用了系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。但是后面改进了,引进了锁的四个状态,分别是无锁,偏向锁,轻量级锁,重量级锁,而且是只能逐级膨胀的。
首先我们要知道,这几个级别适用的场景。
无锁:适用于没有任何线程发生冲突或者争抢的情况,并且没有同步标识
偏向锁:适用于只有一个线程进入同步区
轻量级锁:适用于两个线程交替进入同步区
重量级锁:适用于多个线程同时竞争进入同步区这里我就不赘述有关它们三个的具体,直接讲解如何膨胀。
偏向锁,轻量级锁,重量级锁对比
锁优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景。
轻量级锁竞争线程不会阻塞,提高了程序的响应速度如果始终得不到索竞争的线程,使用自旋会消耗CPU追求响应速度,同步块执行速度非常快重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长
对象头的存储内容(Markword)
长度内容说明
32/64bit Mark Word存储对象的hashcode或锁信息(25bit的hashcode编码)
32/64bit 类对象的地址存储到对象类型数据的指针(klasspointer指针)
32/64bit Array length数组的长度(如果当前对象是数组)
Mark Word存储内容(monitor)的状态变化
25bit(hashcode)、4bit (年代数值)、1bit(是否是偏向锁)、2bit(锁标示位)
轻量级锁指向栈中锁记录的指针 00
重量级锁指向互斥量(重量级锁)的指针 10
锁的状态
锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态
锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁
a线程获得锁,会在a线程的的栈帧里创建lock record(锁记录变量),则在锁对象的对象头里和lock record里存储a线程的线程id.以后该线程的进入,就不需要cas操作,只需要判断是否是当前线程。
a线程获取锁,不会释放锁。直到b线程也要竞争该锁时,a线程才会释放锁。
偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。
如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的所记录。
关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。
轻量级锁
a线程获得锁,会在a线程的栈帧里创建lock record(锁记录变量),让lock record的指针指向锁对象的对象头中的mark word.再让mark word 指向lock record.这就是获取了锁。
轻量级锁,b线程在锁竞争时,发现锁已经被a线程占用,则b线程不进入内核态,让b线程自旋,执行空循环,等待a线程释放锁。如果,完成CAS自旋策略还是发现a线程没有释放锁,或者让c线程占用了。则b线程试图将轻量级锁升级为重量级锁。
重量级锁
重量级锁,就是让争抢锁的线程从用户态转换成内核态。让cpu借助操作系统进行线程协调。
1.当一个线程(假设叫A线程)想要获得锁时,首先检查对象头中的锁标志,如果是偏向锁,则跳转到2,如果是无锁状态,则跳转到3。
2.检查对象头中的偏向线程id是否指向A线程,是,则直接执行同步代码块,不是则3。
3.使用cas操作将替换对象头中的偏向线程id,成功,则直接执行同步代码块。失败则说明其他的线程(假设叫B线程)已经拥有偏向锁了,那么进行偏向锁的撤销(因为这里有竞争了),此时执行4。
4.B线程运行到全局安全点后,暂停该线程,检查它的状态,如果处于不活动或者已经退出同步代码块则原持有偏向锁的线程释放锁,然后A再次执行3。如果仍处于活动状态,则需要升级为轻量级锁,此时执行5.
5.在B线程的栈中分配锁记录,拷贝对象头中的MarkWord到锁记录中,然后将MarkWord改为指向B线程,同时将对象头中的锁标志信息改为轻量级锁的00,然后唤醒B线程,也就是从安全点处继续执行。
6.由于锁升级为轻量级锁,A线程也进行相同的操作,即,在A线程的栈中分配锁记录,拷贝对象头中的Mark Word到锁记录中,然后使用cas操作替换MarkWord,因为此时B线程拥有锁,因此,A线程自旋。如果自旋一定次数内成功获得锁,那么A线程获得轻量级锁,执行同步代码块。若自旋后仍未获得锁,A升级为重量级锁,将对象头中的锁标志信息改为重量级的10,同时阻塞,此时请看7.
7.B线程在释放锁的时候,使用cas将MarkWord中的信息替换,成功,则表示无竞争(这个时候还是轻量级锁,A线程可能正在自旋中)直接释放。失败(因为这个时候锁已经膨胀),那么释放之后唤醒被挂起的线程(在这个例子中,也就是A)。
以上就是我理解的锁膨胀过程。有错误的地方,欢迎指正。
自旋锁,也就是如果持有锁的线程能很短时间内释放锁,那么竞争锁的线程就不需要进入阻塞挂起状态,而是等一会(自旋),这样能避免用户线程和内核线程的切换消耗,但是如果超过一定时间仍未得到,还是会进入阻塞。通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
1.6之后引入了自适应自旋锁,是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。
锁特点分析
偏向锁的加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。
如果只有一个线程访问同步块场景:轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。
如果始终得不到锁竞争的线程使用自旋会消耗CPU追求响应时间,锁占用时间很短重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,锁占用时间较长 。