锁的原理
随着java1.6对synchronized的优化,减少了获取锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,有些情况下它就不会那么重,锁的状态分为4种:无锁、偏向锁、轻量级锁、重量级锁,这几种状态会逐渐升级
对象在内存中的存储布局可以分为对象头、实例数据、对齐填充,对象头中就包含Mark Word,以下是64位Hotspot虚拟机的Mark Word存储结构
查看对象的Mark Word,可以通过在项目中引入ClassLayout
偏向锁 在一些情况下锁总由一个线程多次获得,而不会发生竞争
在锁对象的对象头记录当前获取该锁的线程id,当这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁.而是直接比较对象头里面是否存储了指向当前线程的线程id.如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了
不过如果加锁的对象调用了hashcode方法(如手动调用或者该对象存入hashmap),则会跳过偏向锁直接通过轻量级锁加锁,因为偏向锁没有Displaced Mark word
偏向锁会通过对象头的偏向线程id和栈帧中lock record的线程id匹配直接获取锁,否则使用轻量级锁
轻量级锁 当两个及以上的线程交替获取该锁,但并没有并发获取锁时,偏向锁升级为轻量级锁,线程采用CAS自旋的方式获取锁,避免阻塞线程造成的cpu上下文切换导致的消耗
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间 (Lock Record记录),并将对象头中的Mark word复制到锁记录中,官方称为Displaced Mark word.然后线程尝试使用CAS将对象头中的Markword替换为指向锁记录的指针 (指向线程栈帧里边的Lock Record的指针).如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
轻量级解锁时,会使用CAS操作将Displaced Mark word
替换回到对象头,如果成功则表示没有竞争发生.如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
Mark Word中的内容只是暂时被锁指针占用,临时存储在栈中,并不会丢失
如果锁标志为11,代表被jvm被标记为垃圾的对象
重量级锁 当两个及以上的线程并发的在一个对象上同步时,为了避免面自旋消耗cpu资源,轻量级锁升级为重量级锁.当轻量级锁升级为到重量级锁之后,线程只能被挂起阻塞来等待唤醒了
重量级锁需要操作系统的介入,JVM会创建一个monitor对象,并把这个对象的地址更新到Mark Word中
流程总结
线程1目前为轻量级锁,进入同步代码块,分配空间并记录对象的Mark Word,通过CAS修改Mark Word成功.将该对象Mark Word改为轻量级锁指针
线程1执行同步代码
线程2尝试进入同代码块,和线程1一样希望将对象的Mark Word记录到栈中,但是通过CAS修改Mark Word失败,发现已经被其他线程替换过了.线程2将该对象的Mark Word再次修改为
临时的
指向重量级锁的指针(锁膨胀)线程1执行完同步代码,将栈内存储的Mark Word改回原本存储的(分代年龄、hashcode等),替换失败(因为被线程2修改为重量级锁的指针),释放锁并唤醒等待线程
线程1初始化monitor对象,将monitor内的header属性设置为自己栈中存储的Mark Word
线程1设置锁标志为10,然后将Mark Word前62位设置为
指向重量级锁的指针
,唤醒其他线程线程2被唤醒开始争抢重量级锁(非公平锁)
线程2只是设置了一个临时的重量级锁标记,重量级锁指针是由线程1初始化的,线程2做的只是为了告知此同步代码块产生了并发
字节码指令
通过 javap -p -c 查看字节码指令,可以看到每个monitorenter
指令分别对应了2个monitorexit
指令
当正常执行释放锁时会执行第一个
monitorexit
并通过goto
指令跳过第二个monitorexit
部分当出现异常时,虚拟机会通过异常处理表,也就是会执行第二个
monitorexit
,防止出现异常情况jvm也能释放锁
ObjectMonitor
只有锁升级为重量级锁时才会创建ObjectMonitor,它有如下属性
header 保存对象Markword
own 指向锁持有的线程
cxq 竞争队列
entryList 同步队列
waitset 等待队列
当A线程持有锁,BC线程竞争失败并进入cxq,当A线程释放锁后,BC线程会进入entryList中.假设B线程竞争到了锁并同时调用了Object.wait方法会进入到waitset.当C线程拿到锁后唤醒了B线程,B线程会从waitset中出来,直接竞争锁.如果竞争失败进入cxq继续轮回