首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区;
- 重量级锁:多个线程同时进入临界区。
还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:
synchronized (lockObject) {
// do something
}
上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:
- 情况一:只有Thread#1会进入临界区;
- 情况二:Thread#1和Thread#2交替进入临界区;
- 情况三:Thread#1和Thread#2同时进入临界区。
上述的情况一是偏向锁的适用场景,此时当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操作把Thread#1的线程ID记录到Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。
然而情况一是一个比较理想的情况,更多时候Thread#2也会尝试进入临界区。若Thread#2尝试进入时Thread#1已退出临界区,即此时lockObject处于未锁定状态,这时说明偏向锁上发生了竞争(对应情况二),此时会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,同时锁标识位变为“01”(表示未锁定),这时Thread#2会获取lockObject的轻量级锁。因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。
再说轻量级锁什么时候会膨胀到重量级锁。若一直是Thread#1和Thread#2交替进入临界区,那么没有问题,轻量锁hold住。一旦在轻量级锁上发生竞争,即出现“Thread#1和Thread#2同时进入临界区”的情况,轻量级锁就hold不住了。 (根本原因是轻量级锁没有足够的空间存储额外状态,此时若不膨胀为重量级锁,则所有等待轻量锁的线程只能自旋,可能会损失很多CPU时间)
MarkWord:
锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
长度 | 内容 | 说明 |
32/64bit | Mark Word | 存储对象的hashCode或锁信息等。 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:
| 25 bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 |
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
锁状态 | 25 bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | |||
GC标记 | 空 | 11 | |||
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
|
| cms_free | 分代年龄 | 偏向锁 | 锁标志位 | |
无锁 | unused | hashCode |
|
| 0 | 01 |
偏向锁 | ThreadID(54bit) Epoch(2bit) |
|
| 1 | 01 |
上面是最简单的说法或者理解,但是实际上有很多的切换细节设计性能优化问题。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。(偏向锁只进行一次cas操作)
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。
如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
为什么此时升级为重量级锁呢,将所有竞争线程挂起(重量级锁,是jvm 借助系统级的互斥锁)??假如很多线程都是处于轻量级上,都处于自旋状态,那么其中正在运行一个线程以及释放锁了,但是此时有很多线程在自旋,都在相互竞争。可能造成所有的线程都会一直自旋下去,cpu空转。