【JAVA武器库】synchronized剖析(原理+锁升级流程)

synchronized原理

synchronized是java提供的内置性原子锁。

使用了synchronized的代码块,会在编译之后在代码块的前后加上monitorenter和monitorexit字节码指令。由于只用了monitor这个监视器,synchronized也叫监视器锁,而且由于Java中的线程和操作系统原生线程是一一对应的,1.6之前的可以认为对应操作系统的互斥量(mutex,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。

执行monitorentor会将计数器+1,如果计数器不为0,线程进入等待队列竞争锁。

执行monitorexit后会将计数器-1,当计数器为0时则锁释放,处于等待队列的线程继续竞争锁。

synchronized是排他锁,一旦被一个线程获取到锁之后,别的线程只能等该线程释放锁之后才能获取锁。

一般说到这上面就可以了。

原理实现是两个队列:waitset和entrylist。

  1. 多个线程进入同步代码块时,首先进入entrylist。
  2. 当有一个线程获取到monitor锁之后, 当前线程 =这个线程,把计数器+1
  3. 如果 当前线程 调用了wait方法,会把 当前线程 置null,计数器-1,同时该线程进入waitset,直到调用了notify或者notifyall之后,waitset中的线程重新进入entrylist竞争锁。

synchronized中你烦恼的各种锁介绍

重量级锁

重量级锁也叫 互斥锁、阻塞同步、悲观锁

由于重量级锁的实现是依赖于monitor锁来实现的,而monitor锁又依赖操作系统的互斥锁来实现的,所以重量级锁也叫互斥锁。

为啥重量级所开销大呢,由于线程转换成阻塞和被唤醒的两个过程都是需要操作系统来帮忙的,会涉及到操作系统用户态和内核态的转换,这个过程是开销大的(可能比代码执行时间还长),所以重量级锁开销大。

自旋锁

上面重量级锁提到,线程阻塞状态转换的开销很大,那么自旋锁就是解决这个问题的,自旋锁可以减少线程阻塞造成的线程状态切换,这不起飞。

过程就是:

1.线程1已经占有锁了,线程2来了,一看锁没了,准备阻塞

2.线程2说,诶我等一会呗,来都来了,然后就开始自旋一会儿,如果自旋这会儿拿到锁了,万事大吉。

3.没拿到锁,阻塞。

通过这个过程可以看到,要是竞争不激烈的情况下,自旋锁是具有挺大的优势的。

缺点:

自旋锁虽然看起来挺优秀,但是还是隐含了好些问题;

1.单核cpu的情况,你自旋个屁啊,你不阻塞旧owner就无法执行。

2.自旋的空循环会占用cup,如果是计算密集型认为,就不适合了。

3.如果对锁的竞争普遍时间很长,那每次都自旋都是无意义的,此时应该主动禁用自旋锁。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自适应表示自旋锁的自旋时间不固定了,jvm会根据自旋竞争锁的情况对自旋时间进行收敛,达到合适的值。

轻量级锁

如果根本没有实际的锁竞争,那么申请重量级锁显然很浪费,轻量级锁的目标是,减少实际无竞争的情况下,申请使用重量级锁的消耗。

轻量级锁没有实际的锁住对象,只是用mark word中的部分字节cas指向线程的Lock Record,如果更新成功,则轻量级锁更新成功,否则直接膨胀成重量级锁。

缺点:

也是在锁竞争激烈的情况下,直接就膨胀成重量级锁了,这样维持轻量级锁的过程就显得没必要啦。

偏向锁

偏向锁会比轻量级锁更加轻量级,是对没有实际的锁竞争的场景的进一步优化,假设自始至终使用锁的线程都只有一个,将第一个访问同步代码块的线程初始化记录,后续该线程进入同步代码块就不需要cas去加锁解锁了。具体实现是在对象头和栈桢的锁记录中记录偏向锁的线程id,后续这个线程进入同步代码块就不需要cas来加锁和解锁了。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的消耗。

因此如果有第二个线程进入同步代码快,那偏向锁就会膨胀成轻量级锁。

可以用-XX:+UseBiasedLocking开启偏向锁。

缺点:

同样,如果竞争大,或者有多个线程进入,那么偏向锁很快就膨胀了,但是相比轻量级锁的代价小了很多。

锁消除

在jvm监测到一些同步代码快根本没有锁竞争的情况,就会把锁消除啦

锁粗化

扩大同步锁锁住的范围,比如循环内的锁迁移到循环外,避免对一个代码快反复重复加锁的情况。

锁优化流程

无锁->偏向锁->轻量级锁->重量级锁
在这里插入图片描述

提了半天都对象头到底有哪些内容

虚拟机中对象的组成:

  1. 对象头
  2. 实例数据
  3. 对齐填充

其中实例数据是对象的实例字段、值等数据,也就是咱平时使用的filed

对齐填充是凑数的,因为jvm规定java对象的大小需要是8bit的倍数。

而其中呢,对象头里面的内容也就是咱上文synchronized中反复提到的,所以很有了解的必要。

对象头组成:

1.Mark Word

2.指向类的指针

3.只有数组才拥有的数组长度

其中简单的2是字面意思,让对象晓得自己是那个类的实例

3也是字面意思,数组长度。

1 mark word就是咱上面锁升级过程反复依赖的,它具体被称为 对象自身运行所需的数据 ,具体内容包含:

  • 对象的hashcode
  • 分代年龄
  • 轻量级锁zhizhen
  • 重量级锁指针
  • GC标记
  • 偏向锁线程id
  • 偏向锁时间戳

这样就明显啦:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值