JAVA之所以存在这三种锁,是因为在竞争锁的时候也会消耗资源。为了减少锁本身的资源消耗和响应速度之间的矛盾。就设计了三种锁来平衡。
锁的四种状态:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
- 无锁状态-> 偏向锁状态
偏向锁标识位存储在 锁对象头 Mark Word (默认存储对象的HashCode,分代年龄,锁标记位)上。
当线程获取锁的时候
1.测试对象头Mark Word(默认存储对象的HashCode,分代年龄,锁标记位)里是否存储着指向当前线程的偏向锁。
2.若测试失败,则测试Mark Word中偏向锁标识是否设置成1(表示当前为偏向锁)
3.没有设置则使用CAS竞争,否则尝试使用CAS将对象头的偏向锁指向当前线程
- 偏向锁状态->轻量级锁
轻量级锁是相对于使用底层操作系统mutex
互斥原语实现同步的重量级锁而言的,因为轻量级锁同步的实现是基于对象头的Mark Word
多个线程同时进入临界区会升级为轻量级锁
- 暂停拥有偏向锁的线程,检查线程是否存活
- 处于非活动状态,则设置为无锁状态
- 存活,则重新偏向于其他线程或者恢复到无锁状态或者标记对象不适合作为偏向锁
- 唤醒线程
轻量级锁加锁
如果成功使用CAS将对象头重的Mark Word替换为指向锁记录的指针,则获得锁,失败则当前线程尝试使用自旋(循环等待)来获取锁。
自旋十分消耗CPU性能
- 轻量级锁->重量级锁
轻量级锁自旋到一定程度,就会升级到重量级
其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程,进行竞争。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。