前言
锁的状态总共有四种,级别依次由高到低为无锁->偏向锁->轻量级锁->重量级锁.在jdk1.6以前,只有无锁和重量级锁两种状态,synchronized是一个重量级锁,是一个效率低下的锁.在jdk1.6后JVM引入了偏向锁和轻量级锁,为的是提高synchronized的效率.锁的四种状态会随着线程的竞争逐渐升级,且不能降级.
java对象头
Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word: 默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。 Klass Point: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
下图展示了在64位虚拟机中的对象头:
Synchronized
通过查看Synchronized的字节码可知:在Synchronized的开始处有一个 monitorenter指令,结束处有一个monitorexit指令.monitorenter指令是在编译后插入到同步代码块开始的位置,monitorexit是插在方法结束处和异常处.代码块同步是使用monitorenter和monitorexit指令实现的.
无锁
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
偏向锁
在没有通过JVM启动命令关闭偏向锁的机制时,当一个线程执行到临界区代码块,该线程会尝试使用CAS操作,将自己的线程ID写入对象的MarkWord.此时就完成了无锁到偏向锁的转换. 在临界区代码块执行完毕后,并不会将MarkWord的线程ID赋值回原来的值,如果此时该线程再次对该对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功。
偏向锁的批量再偏向(Bulk Rebias)机制
上面提到说当一个线程获得偏向锁后,执行完了临界区代码块,并不会直接"释放锁",那么是不是别的线程就没机会获得偏向锁了呢?并不是,JVM提供了批量再偏向机制.
在MarkWord中保存了一个epoch值记录,它是一个时间戳,代表偏向锁的有效性.在对象所属的Class信息中也保存了一个和它相等的epoch值记录.每当遇到一个全局安全点时,如果需要进行批量再偏向,则会对Class信息中的epoch根据时间戳进行更新.此时会扫描所有持有 class 实例的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 然后将 epoch_new 的值赋给被锁定的对象中。如果此时某个线程,执行了同步代码块释放了一个对象的锁,那么它的epoch的值与Class信息中的epoch的值不相等,那么该对象的偏向锁就无效了,可以进行对此对象的在偏向.
轻量级锁
如果禁用了偏向锁机制.在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
如果这个更新操作失败了,说明多个线程竞争锁,并且该对象已经被加锁了,此时该线程就进入自旋操作.在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能.如果自旋到一定次数后仍然没有获得锁.轻量级锁就要膨胀为重量级锁,然后其他线程都进入阻塞状态 .
重量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。
|