概述
在JDK1.6中,锁一共四种状态,级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级,这是为了提高获得锁和释放锁的效率。只有重量级锁涉及到操作系统线程切换。
重量级锁
sychronized关键字是通过锁住对象头中的monitor对象来实现同步的。Monitor 的本质是,依赖于底层操作系统的 Mutex Lock 实现。操作系统实现线程之间的切换,需要从用户态到内核态的切换,切换成本非常高。修饰代码块和修饰方法分为了显式同步和隐式同步。当应用代码块的时候,从字节码反编译中可以看到,同步代码块进入开始之前和同步执行之后或者异常以后会有monitor enter 和 monitor exit指令。
而修饰方法的时候是用方法常量池中的ACC_sychronized标志来判断锁的。当某个线程访问这个方法的时候,首先会去检查是否有 ACC_SYNCHRONIZED 有的话就需要先获得对应的监视器锁才能执行。当方法结束或者中间抛出未被处理的异常的时候,监视器锁就会被释放。
无论采用哪种方式, 其本质是对一个对象的监视器(monitor)的获取,这个获取过程是排它的,也就是同一时刻只有一个线程获取到由synchronized所保护对象的监视器。
获取释放逻辑
在 Hotspot 中这些操作是通过 ObjectMonitor 来实现的,通过它提供的功能就可能做到获取锁,释放锁,阻塞中等待锁释放再去竞争锁,锁等待被唤醒等功能。
堆中的每个对象都有一个对象头,存放着minitor对象监视器对象。ObjectMonitor 中有如下几个字段:
_owner,ObjectMonitor 目前被哪个线程持有
_entryList,阻塞队列(阻塞竞争获取锁的一些线程)
_WaitSet,等待队列中的线程需要等待被唤醒(可以通过中断,singal,超时返回等)
_cxq,线程获取锁失败放入 _cxq 队列中
_recursions,线程重入次数,synchronized 是个可重入锁
当多个线程竞争同一把对象锁的时候,会将没有竞争到锁的线程放入_cxq队列中,再根据俄Qmodel的值决定是直接upark竞争锁还是放到到entryList队列中,当有一个线程竞争到对象锁,然后进入owner区域,会把monitor中的owner变量赋值为当前线程,然后计数器加一操作(synchronized 是个可重入锁),当线程调用wait方法的时候,会将锁释放,放入waitset队列中。其他线程可以持有锁。这也就是为什么wait。Notify是Object类中的方法了。
在 jdk1.6 之前,synchronized 就直接会去调用 ObjectMonitor 的 enter 方法获取锁了,然后释放锁的时候回去调用 ObjectMonitor 的 exit 方法。这被称之为重量级锁,可以看出它涉及到的操作复杂性。
所以我们可以想到,如果说同一时间本身就只有一个线程去访问它,那么就算它存在共享变量,由于不会被多线程同时访问也不存在线程安全问题,这个时候其实就不需要执行重量级加锁的过程。只需要在出现竞争的时候再使用线程安全的操作就行了,从而就引出了偏向锁和轻量级锁。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
无锁CAS修改MarkWord成功则升级为偏向锁。
偏向锁
当线程持有偏向锁时,会在Mark Word和该线程的栈帧的锁记录存储锁偏向线程的id,当有线程想要获取锁时,无需再次赋值,只要与Mark Word中的存储的偏向线程ID 与当前线程比较,如果一致的话,则获取到锁(说明当前想获取锁的线程之前就持有锁)。
如果不一致,则需要再测试mark word中偏向锁标识是否为1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS操作将偏向锁指向当前线程(个人理解:一直自旋到全局安全点)
偏向锁的升级
偏向锁撤销要等到全区安全点(这个时间点上没有正在执行的字节码),暂停当前持有偏向锁的线程,此时检查持有偏向锁的线程是否存活。则有两种情况
1、偏向锁的线程已经为终止状态,则将对象头设置为无锁状态,那么竞争线程(正在自旋)会通过cas操作来修改monitor对象头中的变量为自己的偏向锁id。 然后就又回到上述又是偏向锁线程的运行状态了。
2、如果存活,则偏向锁升级为轻量级锁,然后唤醒线程 A 执行完后续操作,其他线程则自旋获取轻量级锁。
为什么要引入偏向锁?
1、为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径。
2、大多数情况下,锁不存在多线程竞争,而且总是由同一线程多次获得,没有必要进行多余的锁获取的代价,为了让线程获得锁的代价更低而引入了偏向锁。偏向锁使用了一种出现竞争才释放锁的机制,当有其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
轻量级锁
加锁
线程在执行同步块之前,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录(Lock Record)的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果成功,则当前线程获取锁,如果失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁,则当前线程自旋等待。若自旋一段时间后(默认10次)后没有获得锁,此时轻量级锁会升级为重量级锁,当前线程(就是自旋一直拿不到的线程)会将Mark Word 的锁标志位变成 10(轻量级锁标志为00),当前线程(就是自旋一直拿不到的线程)会被阻塞。
解锁
使用CAS操作将Lock Record中存储的原来的Mark Word内容替换回对象头,如成功,则说明没有竞争发生。如果失败(由于竞争锁膨胀成了重量锁,就是发现被其他线程修改了对象头中的锁标志),表示当前锁存在竞争,然后它释放锁并且唤醒在等待的线程,锁就会膨胀成重量级锁。
为什么要引入轻量级锁?
因为如果线程竞争不是很激烈,而且对象持有锁的时间不是很长,那么其他线程没必要直接进入阻塞队列,那样的话,会将消耗大量的系统资源,cpu从用户态到内核态(这块可以提一下操作系统进程切换),因此,可以让其他线程自旋一段时间,等待锁的释放,自旋会消耗cpu利用。
锁优缺点对比
参考资料
《java并发编程的艺术》