Synchronize锁原理

synchronized修饰代码块

每个 Java 对象都可以关联一个 Monitor 对象,Monitor对象由jvm提供。当字节码指令执行到monitorenter 指令时,该java对象的对象头mark word指向一个Monitor ,此时该对象可以作为锁,计数器+1。当执行到monitorexit 指令时,计数器-1

当计数器为0时说明没有重入,释放锁!!!!!!!!!!!!!!这也是synchronized的可重入原理。

以上是成功获取锁的情况,在Monitor底层中,如果Monitor已经被关联,则

1首先进行轻量级锁的自旋等待,尝试竞争锁,如果不成功进入ContentionList。这对那些已经在等待队列中的线程来说,显得不公平。

2、进入ContentionList中,ContentionList是一个先入先出虚拟队列,从头部入,从尾部出。当owner线程释放锁后,会从ContentionList尾部迁移线程进入EntryList中。

3、进入EntryList会指定EntryList中的某个线程(一般为队头)为OnDeck线程。OnDeck线程是唯一一个具有竞争资格的线程,需要重新和没有进入Monitor底层的线程竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为竞争切换

4OnDeck线程获得锁后即变为owner线程,没有则会依然留在EntryList中。

5、如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。还要重新获取锁

 

synchronized修饰方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是通过判断Access flags 属性后面有没有synchronized标识,如果有则明了该方法是一个同步方法,JVM 通过该访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用

 

为什么要分两个队列?

目的是为了降低线程的出列速度,防止线程频繁的出队入队。

 

自旋锁

什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环 等待,当线程A释放锁后,线程B可以马上获得锁。

 

引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。

 

自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,一般默认为10。

自适应自旋锁

 

自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。

偏向锁

引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。

 

轻量级锁每次遇到synchronized都需要尝试执行 CAS 替换操作,在没有竞争时比较繁琐。Java 6 中引入了偏向锁来做进一步优化:第一次synchronized时使用 CAS线程 ID 设置到对象的 Mark Word 头(不再存锁记录地址),之后如果遇到synchronized时,检查锁的markword发现这个线程 ID 是自己的就不用重新 CAS。如果发现线程不是自己的,会对原来的线程进行判断,如果原线程不活跃,则进行重偏向;如果原线程是活跃的,则升级为轻量级锁。

 

偏向锁的获取流程:

  1. 检查对象头中Mark Word是否为可偏向状态,如果不是则直接升级为轻量级锁。
  2. 如果是,判断Mark Work中的线程ID是否指向当前线程,如果是,则执行同步代码块。
  3. 如果不是,则进行CAS操作竞争锁,如果竞争到锁,则将Mark Work中的线程ID设为当前线程ID,执行同步代码块。
  4. 如果竞争失败,升级为轻量级锁。

偏向锁的获取流程如下图:

偏向锁的撤销:

只有等到竞争,持有偏向锁的线程才会撤销偏向锁。偏向锁撤销后会恢复到无锁或者轻量级锁的状态。

  1. 偏向锁的撤销需要到达全局安全点,全局安全点表示一种状态,该状态下所有线程都处于暂停状态。
  2. 判断锁对象是否处于无锁状态,即获得偏向锁的线程如果已经退出了临界区,表示同步代码已经执行完了。重新竞争锁的线程会进行CAS操作替代原来线程的ThreadID。
  3. 如果获得偏向锁的线程还处于临界区之内,表示同步代码还未执行完,将获得偏向锁的线程升级为轻量级锁。

 

一句话简单总结偏向锁原理:使用CAS操作将当前线程的ID记录到对象的Mark Word中。

轻量级锁

引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。

Lock Record对象

遇到synchronized后,也是由JVM在每个线程的栈帧中创建

上锁和解锁:

1、遇到synchronized后,在栈帧中创建锁记录Lock Record

2、让锁记录中 Object reference 指向锁对象并尝试用 cas 把锁记录中的自己的地址替换为 Object 的 Mark Word,将 Mark Word 的值存入锁记录 (为什么要交换?看对象头的信息表,轻量级锁对应的markword就是锁记录)

如果 cas 替换成功(对象头mark word状态为01),对象头中存储了锁记录地址和状态 00表示加锁成功。

如果 cas 失败(对象头mark word状态为00),

先检查是否锁重入了,如果是就在栈帧中再添加一个 Lock Record 作为重入的计数

如果是其它线程已经持有了该 Object 的轻量级锁这时表明有竞争,若当前只有一个等待线程(也就是总共两个线程),当前线程便尝试使用自旋来获取锁自旋一定次数获取不到会升级为重量级锁进入锁膨胀过程

3、当退出 synchronized 代码块(解锁时)如果有取值为null 的锁记录,表示有重入,这时去掉锁记录指向Object的指针,表示重入计数减一

4、当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象

成功则解锁成功,mark word变成001的状态

失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

一句话总结轻量级锁的原理:将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。

偏向锁、轻量级锁和重量级锁的区别?

偏向锁的优点是消耗资源非常少,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景

轻量级锁的优点是线程交替使用代码块,不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景

重量级锁的优点是不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值