synchronized
代码示例
1 | public static final Object LOCK = new Object(); |
编译 -> 字节码
1 | // access flags 0x |
Synchronized在修饰同步代码块时,是由monitorenter和monitorexit指令来实现同步的。
进入monitorenter指令后,线程将持有Monitor对象,退出 monitorenter指令后,线程将释放该Monitor对象。(修饰方法,使用标识符,原理相同)
Monitor
JVM中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。Monitor是由ObjectMonitor实现,而ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp文件实现,如下所示
1 | ObjectMonitor() { |
当多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到对象的Monitor时,Monitor是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex。
因Monitor是依赖于底层的操作系统实现,存在用户态与内核态之间的切换,所以增加了性能开销。Synchronized 优化
为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁升级功能
对象头
对象实例在堆内存中被分为了三个部分:对象头、实例数据和对⻬填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组⻓度三部分组成。
Mark Word记录了对象和锁有关的信息。Mark Word在64位JVM中的⻓度是64bit,我们可以一起看下Mark Word在64位JVM的存储结构是怎么样的。如下图所示
锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。
偏向锁
当没有竞争出现时,默认会使用偏向锁。JVM会利用CAS操作,在对象头上的Mark Word部分设置线程 ID,以表示这个对象偏向于当前线程。偏向锁的作用就是,当一个线程再次
访问这个同步代码或方法时,该线程只需去对象头的Mark Word中去判断一下是否有偏向锁指向它的ID,无需再进入Monitor去竞争对象了。
在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加JVM参数关闭偏向锁来调优系统性能。
轻量级锁
当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象Mark Word中的线程ID不是自己的线程ID,就会进行CAS操作获取锁,如果获取成功,直接替换Mark Word中的线程ID为自己的ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前
锁有一定的竞争,偏向锁将升级为轻量级锁。
自旋锁和重量级锁
从JDK1.7开始,自旋锁默认启用,自旋次数由JVM设置决定。
在尝试升级轻量级锁CAS抢锁失败时,会进行自旋,自旋重试上限后依然没有抢到锁,线程将会被挂起进入阻塞状态。升级为重量级锁,这个状态下,未抢到锁的线程都会进入Monitor,被阻塞在_waitSet 队列中。
1 | -XX:-UseBiasedLocking // 关闭偏向锁(默认打开) |
动态编译实现锁消除 / 锁粗化
Java还使用了编译器对锁进行优化。JIT编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步代码块使用的锁对象是否只能够被一个线程访问,编译时直接不生成Synchronized对应的机器码。
锁粗化,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。
代码层优化
减小锁粒度,分段锁..