文章目录
- 1.synchronized作用
- 2.synchronized加锁原理
- 3.monitor锁
- 4.synchronized锁的优化
- 4.1.自适应性自旋锁
- 4.2.偏向锁
- 4.3.轻量级锁
- 4.3.重量级锁
- 5.总结
1.synchronized作用
synchronized是Java提供一种隐式锁
,无需开发者手动加锁释放锁
。保证多线程并发情况下数据的安全性,实现了同一个时刻只有一个线程能访问资源
,其他线程只能阻塞等待,简单说就是互斥同步。
2.synchronized加锁原理
代码块加锁:
例如下面一段代码就是加上了对象锁
这个时候通过反编译查看class字节码信息:
可以看到,底层是通过monitorenter
和monitorexit
两个关键字实现的加锁
与释放锁
,执行同步代码之前使用monitorenter加锁,执行完同步代码使用monitorexit释放锁,抛出异常
的时候也是用monitorexit释放锁。
在方法上加锁:
反编译看一下底层实现
这次只使用了一个ACC_SYNCHRONIZED
关键字,实现了隐式的加锁与释放锁
。其实无论是ACC_SYNCHRONIZED
关键字,还是monitorenter和monitorexit
,底层都是通过获取monitor锁
来实现的加锁
与释放锁
。
3.monitor锁
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
monitor锁是通过ObjectMonitor
来实现的,虚拟机中ObjectMonitor数据结构如下(C++实现的):
其中:
- Owner(持有锁的线程-只能有一个):
存储当前获取锁的线程的
,只能有一个线程可以获取 - EntryList(保存竞争的线程):关联
没有抢到锁
的线程,处于Blocked状态的线程
- WaitSet:关联调用了
wait方法
的线程,处于Waiting状态的线程
执行逻辑:
图上展示了ObjectMonitor的基本工作机制:
- 当多个线程同时访问一段同步代码时,首先会
进入 _EntryList 队列
中等待。 - 当某个线程
获取到对象的Monitor锁
后进入临界区域,并把Monitor中的_owner变量设置为当前线程
,同时Monitor中的计数器_count 加1
。即获得对象锁。
- 若持有Monitor的线程调用
wait()方法
,将释放
当前持有的Monitor锁
,_owner变量恢复为null
,_count减1
,同时该线程进入_WaitSet集合
中等待被唤醒。
- 在_WaitSet 集合中的线程
唤醒后
会被再次放到_EntryList 队列
中,重新竞争获取锁。 - 若当前线程
执行完毕
也将释放Monitor并复位_owner变量的值
,以便其他线程进入获取锁。
4.synchronized锁的优化
JDK1.5之前,synchronized是属于重量级锁
(Monitor实现的锁属于重量级锁),涉及到了用户态和内核态的切换
、进程的上下文切换
,成本较高
,性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁
和轻量级锁
,它们的引入是为了解决在没有多线程竞争
或基本没有竞争
的场景下因使用传统锁机制
带来的性能开销
问题。
4.1.自适应性自旋锁
自旋锁:在没有拿到锁的时候,当前线程会进入阻塞状态
,当持有锁的线程释放了锁,当前线程才可以再去竞争锁。在线程占用锁的时间很短的话。会浪费
大量的性能
在阻塞和唤醒的切换上。
为了避免阻塞和唤醒的切换
,在没有获得锁的时候就不进入阻塞
,而是不断地循环检测锁是否被释放
,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。
自适应性自旋锁:是对自旋锁的一次升级,自适应性自旋锁的意思是,自旋的次数不是固定的
,而是由前一次在同一个锁上的自旋时间
及锁的拥有者的状态来决定
举例就是此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能
最大化利用资源
。
4.2.偏向锁
锁不存在多线程竞争
,而且总是由同一线程多次获得锁
(同一线程可重入锁)
例如如下代码:
同一个线程多次获得锁
加锁m1—>m2—>m3
释放锁m3—>m2—>m1
期间并不存在竞争
,不存在阻塞等待,也不存在唤醒。
原理:
锁的争夺实际上是Monitor对象的争夺
,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。
只有第一次使用
CAS 将线程 ID 设置到对象的 Mark Word 头
,之后发现
这个线程 ID
是自己的
就表示没有竞争
,不用重新 CAS。以后只要不发生竞争
,这个对象就归该线程所有
一旦不同的线程来获取锁的时候,那么偏向锁发现Mark Word中线程id不一样了,就会向上升级为轻量级锁
(不会直接升级到重量级锁)
4.3.轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的
,不同的线程交替的执行
同步块中的代码。(交替执行自然不存在线程竞争)
原理:
执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record)
,并将Mark Word拷贝复制到锁记录中
。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record
。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。
自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。
4.3.重量级锁
也就是上述的synchronized锁
,就是一个重量级锁
重量级锁就是一个悲观锁
了,但是其实不是最坏的锁
,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态
,进入阻塞队列,能减少cpu消耗
。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。
5.总结
Java中的synchronized有偏向锁
、轻量级锁
、重量级锁
三种形式,分别对应了锁只被一个线程持有
、不同线程交替持有锁
、多线程竞争锁
三种情况。
锁 | 对应情况 |
---|---|
偏向锁 | 只被一个线程持有 |
轻量级锁 | 不同线程交替持有锁 |
重量级锁 | 多线程竞争锁 |
锁 | 描述 |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换 、进程的上下文切换,成本较高,性能比较低 。 |
轻量级锁 | 线程加锁的时间是错开的 (也就是没有竞争 ),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志 ,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作 ,之后该线程再获取锁 ,只需要判断mark word 中是否是自己的线程id 即可,而不是开销相对较大的CAS命令 |
一旦锁发生了竞争,都会升级为重量级锁
参考来自:黑马程序员,公众号:java技术爱好者、一灯架构