1.基础知识
1.必须先明确一点,synchronized关键字锁住的是对象而不是代码。
public static final Object m = new Object();
public void func(){
synchronized (m){
doSomeThing();
}
}
这段代码表示一个线程如果要执行同步代码,必须先获取对象m的锁,获取不到就需要等待。
2.Java对象一般是由三部分组成,对象头、实例数据和对齐填充(数组多了一个长度),而对象头又分为markword和class pointer,锁主要通过markword来实现。
2.Markword的组成
一个对象的markword主要由hashcode、分代年龄、偏向锁标示、锁标示、lock record指针、monitor指针、线程id、epoch时间戳组成,但是当对象处于不同的时期,组成成分也不同。
如下:32位的jvm
如下:64位的jvm
从表中可以看到,32位和64位并无太大差别,只是多了一些unused的位。那就以32位的为例来锁一下锁是怎么变更的。
3.锁升级
从上表我们可以看到锁总共被分为了4种类型。无锁、偏向锁、轻量级锁和重量级锁。偏向锁和轻量级锁是从1.6之后才引入的一种机制,是对synchronized的一种优化。
1.锁升级过程图
从图中可以看到分配对象之后会出现两种状况,判断当前类是否允许偏向锁,如果允许创建一个处于可偏向态的对象,如果不允许,创建一个无锁对象。
2.初次获取锁逻辑
当第一个线程获取锁的时候,先判断锁标示位是否是01,依此判断当前对象锁是否处于无锁状态或者偏向锁状态,然后判断偏向锁标示位是否是1,如果不是则进入轻量级锁逻辑,如果是则进入偏向锁逻辑。
3.偏向锁逻辑
首先线程会检查对象锁的markword中记录的thredID是否是当前线程id,如果是则表明当前线程已经获得当前对象锁,那么该线程可直接进入同步块,不需要进行加锁操作,但是会当前线程的栈中添加一条Lock Record中,目的是为了用来统计重入的次数。(线程每次进入同步块,都会以从高往低找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁的时候都会从最低的开始移除。所以如果能找到对应的Lock Record说明偏向的线程还未释放锁。)如果记录的threadid不是当前线程id,那么当前线程会进行cas操作尝试将当前线程id写入markword,如果成功,说明获取到锁,执行同步代码块。如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁。偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态,后续升级为轻量级锁,进行CAS竞争锁。如果允许重偏向,设置为匿名偏向锁状态。最后唤醒暂停的线程,从安全点继续执行代码。
4.Epoch的作用
上面讲述了如果是同一个线程反复获取锁,那么在偏向锁状态时,锁的性能开销几乎时可以忽略不计的,但是当有其他线程来竞争锁时,由于撤销偏向锁需要等到safe point才可以撤销偏向锁,这会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。批量重偏向主要是为了解决一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销机制是为了解决在明显多线程竞争剧烈的场景下使用偏向锁导致的性能下降问题。 首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。每当遇到一个safe point时,比如要对某个类进行批量再偏向,则首先对该类中保存的epoch进行增加操作,得到一个新的epoch_new然后扫描所有持有该类实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。退出安全点后,当有线程需要尝试获取偏向锁时,直接检查该类中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等,则说明该对象的偏向锁已经无效了,此时竞争线程可以尝试对此对象重新进行偏向操作。
5.轻量级锁
如上文所说,当出现了两个或多个线程抢占对象操作时,偏向锁就会升级为轻量级锁。轻量级锁同样使用CAS技术进行实现,它主要说的是多个需要抢占对象操作权的线程,通过CAS的是实现技术持续尝试获得对象的操作权的过程。
如上图所示,当轻量级锁升级为重量级锁时,首先会生成或者复用一个monitor对象(objectmonitor),该对象会与操作系统的互斥量(mutex)和条件变量(condition variable)关联起来,这也正是被称为重量级锁的原因。
当一个线程尝试获取重量级锁的时候,首先会进入cxq竞争队列,以cas的方式尝试获取锁,当超过一定次数后,线程会被挂起。当线程获取锁成功将owner标示为自己后,即表示获取到了锁,开始执行同步块,如果在这期间,调用了wait方法,该线程会再次挂起,让出锁,进入wait list,在wait list的线程只有在调用了notify或者notifyall方法之后才能进入entryset队列继续等待锁。当owner线程执行完成后,如果发现entryset为空的,会将cxq队列中的对象迁移过来,并将队列的的第一个对象标记为OnDeck线程,该线程可以cas的方式继续抢占锁。