Java synchronized实现原理总结和偏量锁、轻量锁、重量锁、自旋锁

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

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

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

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

(重点) synchronized不仅保证了操作的原子性,还保证了操作变量的内存可见性。所以如果仅仅是只考虑内存可见性,synchronized和volatile都能实现,但为了效率,通常是使用volatile。

从JVM规范中可以看到synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步;

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处;

JVM要保证每个monitorenter必须有对应的monitorexit与之配对;

任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态;

线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁;

同步方法,依靠的 是方法修饰符上的 ACC_SYNCHRONIZED 实现;

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

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”;

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,这几个状态会随着竞争情况逐渐升级;

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

锁的表示

Java里的锁,主要都是对对象进行加锁,如普通的synchronized非静态方法,就是对当前执行方法的对象进行加锁。

那么怎么对对象进行加锁呢?对象的锁其实主要就是通过对象头markOop(官方叫做MarkWord)进行表示的。markOop其实不是一个对象,只是一个字长的数据,在32为机器上,markOop为32个位,在64位上为64个位。

markOop中不同的位区域存储着不同的信息,但是需要注意的一点是,markOop每个位区域表示的信息不是一定的,在不同状态下,markWord中存着不同的信息。

下图很重要,是synchronized锁状态的对象头表示:

理解性记忆对象头可以从无锁->偏向->轻量->重量->GC标记 方式对比记忆,对象头里锁标志位一次是01->01->00->10->11

1 偏向锁

很多情况下,一个锁对象不会发生被多个线程访问得情况,更多是被同一个线程进行访问,如果一个锁对象每次都被同一个线程访问,根本没有发生并发,但是每次都进行加锁,那岂不是非常耗费性能,所以偏向锁就被设计出来了;

偏向,也可以理解为偏心;

当锁对象第一次被某个线程访问时,它会在其对象头的markOop中记录该线程ID,那么下次该线程再次访问它时,就不需要进行加锁了;

一个线程访问同步块并获取锁时,会在对象头栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁;

如果测试成功,表示线程已经获得了锁;如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS对象头的偏向锁指向当前线程(线程ID);

但是这中间只要发生了其他线程访问该锁对象的情况,证明这个锁对象会发生并发,就不能对这个对象再使用偏向锁了,会进行锁的升级

2 轻量级锁

有竞争且竞争不强烈时,JVM就会由偏向锁膨胀为轻量级锁,考虑到线程的阻塞和唤醒需要CPU从用户态转为核心态(增加CPU负担),而这种转换对CPU来说是一件负担很重的操作,因此没有获取到锁的线程不会进入阻塞状态,而是通过自旋的方式一直尝试获取锁,处于一种忙等状态,所以这种处理竞争的方式比较浪费CPU,但是响应速度很快

3 重量级锁

竞争激烈时,不会立刻进入阻塞状态而是会自旋一段时间看是否能获取锁如果不能则进入阻塞状态。

三种锁的特点

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距 如果线程间存在锁竞争,会带类额外的锁撤销的消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争几乎不使用自旋,不会消耗CPU 阻塞线程,响应时间缓慢 追求吞吐量,同步块执行速度较长

自旋锁

例子:死循环内sun.misc.Unsafe对象的CAS操作

来源:

https://blog.51cto.com/wenshengzhu/2062647

https://juejin.im/entry/5aad7773518825558252231a

https://juejin.im/post/5ae6dc04f265da0ba351d3ff

原文地址:https://www.cnblogs.com/theRhyme/p/11084189.html