在多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源。由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

Monitor监视器

在Java中任何一个对象都有一个Monitor(可以认为是一个对象)与之关联,因为在Java的设计中 ,每一个Java对象被创建出来就带了一把看不见的锁,它叫做内部锁或者Monitor 锁,也就是通常说Synchronized的对象锁(底层基于管程机制实现),当且一个Monitor 被持有后,它将处于锁定状态。

ObjectMonitor

HotSpot中的ObjectMonitor的定义:

  1. ObjectMonitor::ObjectMonitor() {
  2. _header = NULL;
  3. _count = 0;
  4. _waiters = 0,
  5. _recursions = 0; //线程的重入次数
  6. _object = NULL;
  7. _owner = NULL; //标识拥有该monitor的线程
  8. _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet队列
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ; //管程中的紧急等待队列
  13. FreeNext = NULL ;
  14. _EntryList = NULL ; //管程的入口等待队列,处于等待锁block状态的线程,会被加入到该列表,等待所释放争夺锁资源
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. }

Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,底层是通过成对的monitorentermonitorexit指令来实现。
微信截图_20210629110630.png
monitorenter

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者;
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝 试获取monitor的所有权;

monitorexit

  1. 执行monitorexit的线程必须是objectref所对应的monitor的所有者,指令执行时,monitor的进入数减1
  2. 如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。
  3. 其他被这个monitor阻塞的线程可以尝试去争夺这个monitor。

锁的升级过程

上述的加锁过程是依赖于一个互斥信号量 Mutex,Java程序需要从用户态切换到内核态,这种切换比较重的操作,当我们同步块中的业务代码不多的时候,可能切换的时间比代码执行的时间都要久,所以在JDK1.6及以后对锁进行了一系列优化。锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁

对象头的中的 “Mark Word” 用于存储对象自身的运行时数据, 如哈希码 (HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。Mark Word被设计成一个非固定的 数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,下图以32位为例:
微信截图_20210710093615.png

偏向锁

在大多数情况下,锁不仅不存在多线程竞 争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
微信截图_20210710183303.png
当获取偏向锁的线程没有退出同步代码块时,如果有线程争夺锁资源,此时锁会升级为轻量级锁

注意:当获取偏向锁的线程退出同步代码块时,并不会去修改对象 markwork 的结构,因为偏向锁的本意就是让该锁偏向某一个线程,该锁一直被一个线程持有。当获取便向锁的线程下一次进入同步代码块时,只需要判断一下 markword 中的线程ID与当前线程ID是否相同就可以,不需要额外的操作。因此为了消除数据在无竞争情况下锁重入(CAS 操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。

延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等,在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。

  1. //关闭延迟开启偏向锁
  2. XX:BiasedLockingStartupDelay=0
  3. //禁止偏向锁
  4. XX:‐UseBiasedLocking
  5. //启用偏向锁
  6. XX:+UseBiasedLocking

验证例子如下:

  1. public class LockEscalationDemo{
  2. public static void main(String[] args) throw InterruptedExcepiton{
  3. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  4. Thread.sleep(4000);
  5. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  6. }
  7. }

偏向锁的重偏向和撤销

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的 Mark Word 中也有该字段,其初始值为创建该对象时class 中的 epoch 的值。每次发生批量重偏向时,就将该值+1,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其 epoch 字段改为新值。下次获得锁时,发现当前对象的 epoch 值和 class 的 epoch 不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过 CAS 操作将其 Mark Word 的 Thread Id 改成当前线程Id。

当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后对于该class的锁,直接走轻量级锁的逻辑。 设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值:

  1. intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
  2. intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值

轻量级锁

线程在进入同步块之前(不管后续获取的是什么锁),如果当前对象还是无锁状态,虚拟机会在当前线程栈帧中开辟一块 “Lock Record“ 空间,存放一份对象无锁状态下的Mark Word 副本。获取轻量级锁的过程就是通过CAS尝试把对象 Mark Word 的指针指向某个线程栈中的Lock Record

注意:如果当前对象是偏向锁状态,则需要进行偏向锁撤销,在由无锁状态变为轻量级锁。因为轻量级锁需要拷贝一份 markword 副本。
微信截图_20210710180036.png
释放轻量级锁只需CAS将线程栈中Lock Record 拷贝的Mark Word 与当前对象的Mark Word替换回来。如果成功,同步过程就完成;如果失败,则说明有其他线程尝试过获取该锁,要在释放的同时唤醒被挂起的线程。
微信截图_20210710193521.png
如果多个线程同时争夺轻量级锁(底层执行ObjectMonitor的enter()方法,该方法会将线程挂起前进行一系列自旋操作),则会膨胀为重量级锁。

重量级锁

底层通过争夺操作系统的互斥量(Mutex)从而得到 Monitor 对象锁,具体过程如下
微信截图_20210710201513.png

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次 自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。Java 7 之后不能控制是否开启自旋功能。

关于对象HashCode的问题

细心的读者会发现,当对象进入了偏向锁状态,Mark Word中并没有空间来存放对象的HashCode,那么此时要获取对象的HashCode怎么办呢?
微信截图_20210710202206.png

我们来看一看Hotspot底层如何获取对象的HashCode

  1. intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  2. if (UseBiasedLocking) {
  3. //如果之前使用过偏向锁
  4. if (obj->mark()->has_bias_pattern()) {
  5. // Box and unbox the raw reference just in case we cause a STW safepoint.
  6. Handle hobj (Self, obj) ;
  7. assert (Universe::verify_in_progress() ||
  8. !SafepointSynchronize::is_at_safepoint(),
  9. "biases should not be seen by VM thread here");
  10. //解除偏向锁状态,下次加锁直接变为轻量级锁
  11. BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
  12. obj = hobj() ;
  13. assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  14. }
  15. }
  16. assert (Universe::verify_in_progress() ||
  17. !SafepointSynchronize::is_at_safepoint(), "invariant") ;
  18. assert (Universe::verify_in_progress() ||
  19. Self->is_Java_thread() , "invariant") ;
  20. assert (Universe::verify_in_progress() ||
  21. ((JavaThread *)Self)->thread_state() != _thread_blocked, "invariant") ;
  22. ObjectMonitor* monitor = NULL;
  23. markOop temp, test;
  24. intptr_t hash;
  25. markOop mark = ReadStableMark (obj);
  26. // 再次确保当前对象不是偏向锁状态
  27. assert (!mark->has_bias_pattern(), "invariant") ;
  28. //如果当前对象是无锁状态
  29. if (mark->is_neutral()) {
  30. hash = mark->hash();
  31. //已经计算过hashcode直接返回
  32. if (hash) {
  33. return hash;
  34. }
  35. hash = get_next_hash(Self, obj);
  36. temp = mark->copy_set_hash(hash); // merge the hash code into header
  37. test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
  38. if (test == mark) {
  39. return hash;
  40. }
  41. // If atomic operation failed, we must inflate the header
  42. // into heavy weight monitor. We could add more code here
  43. // for fast path, but it does not worth the complexity. -----上述原子操作失败需要膨胀为重量级锁
  44. //如果当前对象是重量级锁状态
  45. } else if (mark->has_monitor()) {
  46. monitor = mark->monitor();
  47. temp = monitor->header();
  48. assert (temp->is_neutral(), "invariant") ;
  49. hash = temp->hash();
  50. if (hash) {
  51. return hash;
  52. }
  53. // 如果当前对象是轻量级锁状态
  54. } else if (Self->is_lock_owned((address)mark->locker())) {
  55. //从线程栈中的MarkWord副本获取hashcode
  56. temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
  57. assert (temp->is_neutral(), "invariant") ;
  58. hash = temp->hash(); // by current thread, check if the displaced
  59. if (hash) { // header contains hash code
  60. return hash;
  61. }
  62. }
  63. // 以上情况都不是,说明之前是偏向锁状态,直接膨胀为重量级锁
  64. monitor = ObjectSynchronizer::inflate(Self, obj);
  65. // Load displaced header and check it has hash code
  66. mark = monitor->header();
  67. assert (mark->is_neutral(), "invariant") ;
  68. hash = mark->hash();
  69. if (hash == 0) {
  70. hash = get_next_hash(Self, obj);
  71. temp = mark->copy_set_hash(hash); // merge hash code into header
  72. assert (temp->is_neutral(), "invariant") ;
  73. test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
  74. if (test != mark) {
  75. hash = test->hash();
  76. assert (test->is_neutral(), "invariant") ;
  77. assert (hash != 0, "Trivial unexpected object/monitor header usage.");
  78. }
  79. }
  80. return hash;
  81. }

可以看出,当对象处于偏向锁状态是没法获取hashcode的。当一个对象已经计算过hashcode,那么该对象无法进入偏向锁状态,下次抢锁直接为轻量级锁。如果在获取偏向锁的同步代码块中获取hashcode,则直接膨胀为重量级锁

Synchronize的使用

静态方法上

锁的是当前类的Class对象

  1. public class LockOnClass {
  2. static int stock;
  3. public static synchronized void decrStock(){
  4. System.out.println(--stock);
  5. }
  6. public static synchronized void cgg(){
  7. System.out.println();
  8. }
  9. public static void main(String[] args) {
  10. //锁的是LockOnClass.class对象
  11. Juc_LockOnClass.decrStock();
  12. }
  13. }

普通方法上

锁的是创建该对象的this对象,粒度大,获得锁后该对象的其他方法都没法调用

  1. public class LockOnThisObject {
  2. private Integer stock = 10;
  3. public synchronized void decrStock(){
  4. --stock;
  5. System.out.println(ClassLayout.parseInstance(this).toPrintable());
  6. }
  7. public static void main(String[] args) {
  8. LockOnThisObject to = new LockOnThisObject();
  9. to.decrStock();
  10. LockOnThisObject to1 = new LockOnThisObject();
  11. to1.decrStock();
  12. }
  13. }

代码块中

锁的是new出来的对象

  1. public class Juc_LockOnObject {
  2. public static Object object = new Object();
  3. private Integer stock = 10;
  4. public void decrStock(){
  5. synchronized (object){
  6. --stock;
  7. if(stock <= 0){
  8. System.out.println("库存售罄");
  9. return;
  10. }
  11. }
  12. }
  13. }