Java加锁的优缺点_synchronized(偏向锁和轻量级锁)(TODO)

synchronized

JDK1.6对synchronized进行了各种优化,性能已经和ReentrantLock差不多了。

Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

对于普通同步方法,锁是当前实例对象。

对于静态同步方法,锁是当前类的Class对象。

对于同步方法块,锁是Synchonized括号里配置的对象。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。

代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。

但是,方法的同步同样可以使用这两个指令来实现。

Java对象头

如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。

Java对象头的长度

20190708001302869307.png

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。

Mark Word的存储结构

20190708001303023610.png

Mark Word的状态变化

20190708001303217954.png

在64位虚拟机下,Mark Word是64bit大小

64bit的Mark Word的存储结构

20190708001303373233.png

锁升级与对比

JDK1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。

偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出

同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否

存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需

要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则

使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,

持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正

在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,

如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈

会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他

线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

偏向锁初始化

20190708001303475776.png

书上说了一大堆,简单来说就是分两种场景,一种是有竞争,一种是无竞争

无竞争:

无竞争的场景可以假设是一个单线程请求synchronized锁,当第一次请求锁时,锁的对象头中的偏向锁线程ID是空,状态是0。

锁请求成功后,JVM会将这个线程ID写入对象头,偏向锁线程ID不为空,状态是1,然后该线程执行完同步代码后释放锁,接着又继续请求获取锁。

然后判断锁的请求头中的偏向锁状态是否为1,为1则再判断偏向锁的线程ID是否是该线程ID,如果是则直接进入临界区执行同步代码,无需加锁和解锁的操作。

有竞争:

有竞争的场景的第一个请求锁的线程同无竞争场景一样,假设线程A请求锁成功,设置偏向锁状态为1,记录偏向锁线程ID,但由于存在锁竞争,第二个线程B请求锁时,发现偏向锁状态是1,

但是锁的对象头中偏向锁记录的线程ID并不是自己,这时就需要撤销偏向锁,撤销之前必须要检查线程A是不是还活着,因为此时线程A可能正在临界区内执行同步代码,也可能线程A活的很滋润,

在另外一把锁的临界区内执行同步代码,也可能代码执行完毕已经释放锁然后线程池中等待复用,也可能代码执行完毕且线程已经死亡。

所以线程B请求锁时发现偏向锁的线程ID不是自己,JVM会暂停对象头中记录的偏向锁ID。

如果线程A暂停失败说明线程死亡,线程B直接将对象头设置成无锁状态

如果线程A暂停成功说明线程A活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,

要么锁状态恢复到无锁(锁不升级)或者标记锁对象不适合作为偏向锁(升级为轻量级锁),最后唤醒暂停的线程。

这里有个疑惑,TODO 一下,什么情况锁不升级,什么情况锁才升级?

我的想法是,由于锁只能升级不能降级,那么盲目升级锁势必会增大锁开销而降低性能。所以线程B发现锁的偏向线程ID不是自己还需要一些判断再决定是否需要升级锁。

即如果线程A活着,但是线程A处于挂起状态或者当前线程A在活动中且栈信息中的锁信息不是线程B请求的锁,则锁状态置位无效,不升级锁

如果线程A正在锁的临界区执行同步代码,则需要锁升级,升级为轻量级锁。

所以总结下来,偏向锁请求过程如下:(这里只说偏向锁的请求,即所标志为=01的情况)

判断锁对象的对象头中的Mark Word是否为无锁状态(锁状态=0)

a.是,CAS设置锁状态为1,偏向锁ID为当前线程ID

b.否:判断锁状态是否为1

a.是,继续判断偏向锁ID是否为当前请求线程ID

a.是,无需加锁和解锁,直接进入临界区执行同步代码

b.否,判断偏向锁ID对应的线程是否存活

a.是,暂停线程,判断线程栈中的锁对象信息是否为当前请求的锁对象信息

a.是,存在竞争,当前线程请求的锁已被其他线程占用且正在临界区执行同步代码,锁升级为轻量级锁,在暂停的线程栈中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到                 锁记录中,使用CAS将对象头中的Mark Word替换为指向锁记录的指针,恢复之前暂停的线程

b.否,没有线程在当前锁对象的临界区内,修改锁的偏向线程ID为当前线程或重置为无锁状态,CAS设置锁状态为1,偏向锁ID为当前线程ID。恢复之前暂停的线程

b.否,锁已升级,不使用偏向锁

轻量级锁

1)加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并

将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用

CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失

败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

2)解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成

功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

锁膨胀流程

20190708001303695511.png

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级

成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,

都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮

的夺锁之争。

总结轻量级锁请求过程:

判断锁对象的对象头中的Mark Word是否为无锁状态(锁状态=0)

a.是,在栈帧中创建锁记录空间,复制锁对象头的Mark Word到锁记录中,CAS将锁对象的对象头中的Mark Word替换为指向锁记录的指针。

a.替换成功,则加锁成功,执行同步代码,执行完毕后CAS操作将Displaced Mark Word替换回到对象头进行解锁

a.解锁成功,此时锁状态为无锁状态

b.解锁失败,说明当前线程在执行同步代码的时候有其他线程来请求锁资源,且请求失败将锁升级为重量级锁

b.替换失败

自旋,走下面的自旋流程

b.否,检查对象的Mark Word是否指向当前线程的栈帧的锁记录

a.是,说明当前线程已持有锁,直接进入同步代码

b.否,自旋,空循环自旋一段时间

a.自旋成功,获取锁成功

b.自旋失败,升级锁为重量级锁,修改锁对象的Mark Word指针指向重量级锁

锁的优缺点对比

20190708001303802937.png

参考:

《Java编发变成艺术》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值