Java中锁的四种状态以及锁升级过程
Java中加锁的最简单方式就是加synchronized关键字,但它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。
Synchronized 优化
锁升级
在Java中锁的状态一共有四种,级别由低到高分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。
1. 对象头
在Java中锁不是某一个具体的实物资源,而是对象上的某个标记,而这个标记就记录在对象头上。
Mark Word(对象头)内部:
2. 无锁
对象头中有1bit来表示是否是偏向锁,2bit存放锁标志位,偏向锁位与锁标志位合起来“001”就代表无锁。无锁就是没有对任何资源进行锁定,所有线程都能访问并修改资源。
3. 偏向锁
对象头中记录了获得偏向锁的线程ID,偏向锁与锁标志位合起来“101”就代表偏向锁。
为什么要引入偏向锁?
有研究发现大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级
当线程访问代码块并获取锁对象时,会将获得锁的线程ID写入到锁对象Mark Word中。当线程访问资源结束后,不会主动释放锁。当线程再次需要访问资源时,JVM就会通过Mark Word中记录的线程ID判断是否是当前线程,如果是就继续访问资源,无需使用CAS来加锁、解锁。
所以在没有其他线程参与竞争时,锁就一直偏向被当前线程持有,当前线程就可以一直占用资源或者执行代码。
如果不一致,需要查看Mark Word中记录的线程是否存活:
- 如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁
- 如果存活,那么立刻查找该记录线程的栈帧信息
- 如果该记录线程还需要继续持有这个锁对象,那么会撤销偏向锁,升级为轻量级锁
- 如果该记录线程不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程
4. 轻量级锁(自旋锁)
一旦有另外一个线程参与锁竞争,偏向锁就会升级为轻量级锁(自旋锁),此时撤销偏向锁,锁标志位变为“00”。
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。
因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁的升级
竞争的两个线程都在各自的线程栈帧中生成一个Lock Record空间,用于存储锁对象目前Mark Word的拷贝(称为DisplacedMarkWord),用CAS操作将Mark Word设置为指向自己这个线程的Lock Record指针,设置成功者获得锁,其他参与竞争的线程如果未获取到锁,则会一直处于自旋等待的状态,直到竞争到锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次。当竞争的线程自旋次数到了,然而持有锁的线程还没有释放锁,这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
5. 重量级锁
长时间的自旋操作是很消耗CPU资源的,为了避免这种盲目的消耗,JVM会在有线程超过10次自旋,或者自旋次数超过CPU核数的一半(JDK1.6以后加入了自适应自旋-Adaptive Self Spinning,由JVM自己控制自旋次数)时,会升级到重量级锁。
重量级锁底层是依赖操作系统的mutex互斥锁,也就是有操作系统来负责线程间的调度。
重量级锁减少了自旋锁带来的CPU消耗,但是由于操作系统调度线程带来的线程阻塞会使程序响应速度变慢。
6. 偏向锁、轻量级锁、重量级锁的对比
锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。