synchronized和锁的多种状态
一、对象头
每个实例对象都有自己对应的对象头;
对象头包含Mark word(32位) 和 Klass word(32位)两部分,Klass word是一个指针,指向对象从属的Class对象;数组对象的话,还有一个用于表示数组长度的部分,占用32位也就是4个byte。
对象头结构:
一般分为普通对象和数组对象

Mark word的结构:前25位表示hashcode,再后面4位表示分代年龄,再下面一位biased_lock表示是否为偏向锁,再后面两位表示加锁状态;
Mark word在对象锁处于不同状态时,有不同的结构:

锁记录 + 00表示轻量级锁;Monitor对象 + 10表示重量级锁;threadID + 01表示偏向锁,偏向锁状态时保存的时线程id;最上面的01表示无锁。
二、Monitor(监视器或管程)
1、每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象lock上锁之后,lock的对象头中的Mark word就指向Monitor对象的地址。
2、Monitor对象内有三个重要属性:WaitSet,waiting状态线程队列(等待队列);EntryList,blocked状态线程队列(阻塞队列);Owner,拥有锁的线程对象指针,表示该线程占有该对象锁。
3、轻量级锁、重量级锁、偏向锁:
每个线程都包含一个锁记录结构,属于jvm层面的概念,内部存储锁定对象的MarkWord。

object ref表示对象引用,指向锁定对象的地址。
代码执行到synchronized关键字时,首先在线程对象内创建锁记录,然后让锁记录中的obj ref指向锁对象,并尝试用cas替换Object的Mark word,将其保存到锁记录中;如果cas替换成功,锁记录中就保存了Object的Mark word,而且Object的Mark word被替换成了线程的锁记录的地址和状态;

锁重入:同一个线程对同一个Object再次加锁,线程对象内将会再创建一个锁记录,并且尝试cas操作,cas失败,所以锁记录为null,这时如图:

第二个锁记录结构中锁记录地址为null。新的锁记录作为锁重入计数存在。
轻量级锁解锁:退出synchronized代码块时,如果有取值为null的锁记录,表示有重入,需要重置锁记录,这个lock record就去除了,重入计数也减一,一直到只剩第一次加锁的锁记录;如果锁记录的值不为null,就会进行cas将Mark word的值恢复给锁定对象,如果cas失败,表示轻量级锁已经升级为重量级锁,将会进入重量级锁解锁流程。
锁膨胀和重量级锁: 当一个线程thread0已经给对象上了轻量级锁时,这时另一个线程thread1也尝试给这个对象加锁,cas失败,然后进入锁膨胀流程,就是thread1线程为Object对象申请一个Monitor锁,让Object的Mark word指向这个Monitor重量级锁地址,然后thread1线程自己进入Monitor的EntryList阻塞队列进行阻塞。
重量级锁解锁过程:通过Object中Mark word保存的Monitor的地址,找到Monitor,将其Owner设为null,并将EntryList中阻塞线程唤醒。
自旋优化:重量级锁竞争时,第一次获取锁失败,然后线程进行自旋重试,多次尝试获取锁,如果获取成功,就不用进入阻塞队列,避免阻塞。注意:自旋本身就要占用cpu资源,需要在多核cpu情况下才有意义,否则单核cpu自旋就意味着要停下正在进行的其他任务,这样其实是资源浪费。java7以后,自旋不能用户自己开启,而是jvm本身的设置。偏向锁默认是开启的,设置关闭的jvm参数:-xx:-UseBiasedLocking ,表示禁用。
自旋自适应:java6以后,自旋是自适应的,一次自旋成功后,下次另外的线程再来获取锁时自旋次数会更多,反之,一次自旋失败后,下次自旋重试的次数就会减少。
偏向锁:轻量级锁在没有其他线程竞争时,每次重入也需要进行cas操作,为了避免cas操作带来的性能消耗,java6中引入了偏向锁来进行优化;只要第一次使用cas操作将线程id设置到所对象的对象头中的Mark word中,之后锁重入时,判断如果是同一个线程,就不进行cas操作了。
偏向锁撤销:调用锁对象的hashcode方法会导致偏向锁撤销,因为hashcode占用字节数较多,偏向状态下的Mark word空间不够,只能回复到normal无锁状态才能存储这个hashcode值;另外导致偏向锁撤销的情况就是,发生线程竞争锁,原来的偏向锁撤销,升级为轻量级锁;调用wait/notify也会将偏向撤销,这两个方法是重量级锁状态下的方法,所以调用方法时会将锁升级为重量级锁。
批量重偏向:当对某个类的对象偏向锁批量撤销20次,则偏向锁认为,后面的锁需要重新偏向新的线程(批量重偏向)。当达到阈值20次时,就发生重偏向,20次之后尝试锁竞争时,这个时候锁会重新偏向thread1线程。偏向锁撤销比较耗费性能,所以批量重偏向机制也是一种优化。
批量撤销:若同一个类的对象的偏向锁撤销次数达到阈值40次,之后这个类对象(不管是新建的还是已有的对象)的锁都是不可偏向的了,直接变成轻量级锁。
锁消除:jit即时编译器的优化机制,其会将编译后的字节码进行优化,如果发现synchronized代码块内没有发生多线程共享变量,就会消除synchronized。开关时jvm默认打开的,也可通过参数关闭:-xx:-EliminateLocks 。
偏向锁,轻量级锁及重量级锁升级过程:
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

浙公网安备 33010602011771号