锁优化与锁升级

430 阅读10分钟

在JDK1.5之后,JVM开发团队对Synchronized关键进行了优化,并且实现了各种锁优化技术,并且各种测试表明Synchronized并不比ReentrantLock慢,下面就一起看看Synchronized的锁升级以及有哪些锁优化吧~

自旋锁和自适应自旋锁

自旋锁
  • 什么是自旋锁

    是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

    自旋锁的默认自选次数是10次,可以通过JVM参数-XX:PreBlockSpin进行修改

  • 为什么需要自旋锁

    Java的线程是映射到操作系统的原生内核线程之上的(线程是通过操作系统的内核线程实现的,并且创建出来的线程通过操作系统内核进行调度),如果要阻塞或唤醒一条线程,则需要操作系统来完成,而应用程序运行在用户空间,线程需要在内核空间中进行调度,不可避免的需要在内核态和用户态之间进行转换,这种转换需要耗费很多处理器的时间,所以重量级锁的操作是非常耗时也是因为线程的阻塞和唤醒。

    自旋锁的缺点: 自选次数无法确定,如果自选可以获取到锁,那么自选就是有价值的,但是如果一直自选都无法获取到锁,那么就会一直在浪费CPU资源。所以JVM中自选的默认次数是十次,如果超过自选次数没有获取到锁,就会将线程阻塞。使用JVM参数-XX:PreBlockSpin修改自选次数

自适应自旋锁
  • 什么是自适应自旋锁

    自适应自旋锁就是为了解决自旋锁的缺点,防止过多自旋获取自选可以获取到锁,但是自旋的次数到了的问题。自适应自旋即自旋的次数和时间不是固定的了,而是通过前一次的经验来判断,如果前一次在同一锁上通过自选获取到了锁,那么会认为这次也可以获取到锁,则自选的次数和时间就会更多一些,如果自旋的过程中很少获取到锁,那么可以减少自旋或直接不自旋,将线程阻塞,因为自旋也是浪费CPU资源。

    通过自适应自旋,程序执行的时间越长,那么JVM对锁的信息情况就会越来越准确,自适应自旋的价值就越大。

锁消除

  • 什么是锁消除

    锁消除是指在JVM在JIT即时编译时优化代码的一种方式(想了解JIT即时编译的小伙伴看这篇文章哦运行期编译优化),主要是将不需要同步的代码从同步代码块中移除,提升执行效率。

  • 锁消除原理

    锁消除可以实现的原理是基于JIT编译的优化方式之一—逃逸分析,当代码没有发生逃逸,会将该变量分配在栈上,那么该对象没有逃逸到线程之外,则该对象就是线程私有的,不会出现多线程的数据一致性和安全性问题,则该这样的代码就会被移除到同步代码块之外,无需进行同步。

锁粗化

  • 什么是锁粗化

    和锁消除相反,锁消除是将同步代码块的作用域变小,而锁粗化的目的是将锁的同步代码块的作用域变大,当然,同步代码块的作用域变大也是为了提高程序的执行效率。例如下面的代码:

    public void test() {
        for(int i = 0; i < 100; i++) {
            sychronized(Test.class) {
                System.out.println(i);
            }
        }
    }
    

    看似代码没有问题,但是每次循环都会进入一次同步代码块,获取一次锁,降低性能。而锁粗化的目的就是将同步代码块的作用域,防止重复获取锁的过程,只需要加锁一次就可以。

    锁粗化之后的代码:

    public void test() {
        sychronized(Test.class) {
        	for(int i = 0; i < 100; i++) {
                System.out.println(i);
            }
        }
    }
    

对象头—MarkWord

在了解sychronized锁升级之前,我们先了解一下Java中的对象头,准确点说是对象头中的MarkWord区,锁升级和MarkWord区中存储的锁标志位息息相关。

  • 对象头

    Java的对象由3部分构成,对象头、实例数据、对齐填充。其中,对象头是不可操作的,信息由JVM生成和改变。

    对象头又分为MarkWord类型指针。如果是数组还会额外保存数组的长度

  • MarkWord

    MarkWord区主要保存了GC年龄、hashcode(JVM生成hashcode之后就会保存到对象头中)、锁标志位等信息

    image-20210328215633274

    其中偏向锁和无锁的锁标志位是相同的,因为偏向锁基本上就相当于无锁状态,两个状态可以根据偏向模式进行区分。

    另外:注意偏向锁会包含GC年龄、hashcode、持有锁的线程ID等信息在MarkWord中,对于轻量级锁、重要级锁已经没有这些信息,只保存了指针,轻量级锁和重量级锁没有这些信息吗?当然不是,每个对象都会有这些信息,因为轻量级锁和重量级锁保存的是指向对应的锁的指针,所以指针指向的锁对象包含了这些信息在锁的对象头中。

    轻量级锁MarkWord区的指针指向的是:持有锁线程的栈中的栈记录空间

    重量级锁MarkWord区的指针指向的是:ObjectMonitor对象(不知道ObjectMonitor对象结构的在下面哦~)

偏向锁

  • 什么是偏向锁

    偏向锁是JDK1.6中引入的一项优化措施,它的目的是消除数据在没有多线程竞争的条件下的同步,偏向锁相当于没有锁,只是在获取到偏向锁时,将当前锁的偏向模式打开,那么每次在进入同一个锁对象的同步代码块时,无需重复的获取锁,减少了加解锁的操作。

  • 偏向锁的加锁流程

    当一个线程第一次获取到锁对象时,虚拟机会将锁对象头中的标志位修改为“01”(其实本来就是01,无锁状态的锁对象头中的标志位就是01),然后将锁对象头中的偏向模式设置为“1”,表示进入到偏向模式。并使用CAS操作将当前获取锁的线程ID更新到锁对象头的Mark Word区。并且持有偏向锁的线程在每次进入该锁对象的同步代码块时,如果发现当前为偏向模式,并且锁对象头中记录的线程ID为当前线程,那么无需进行加解锁操作,直接执行代码。

  • 偏向锁升级

    一旦当有其他线程尝试获取锁对象时,偏向模式就立即结束,根据当前锁对象是否处于被锁定状态决定是否撤销偏向模式(即将偏向模式设置为“0”),将锁标志位设置为“01”—无锁定状态(如果当前处于未被锁定状态),或者设置为“00”—轻量级锁模式(如果当前为被锁定状态),那么偏向锁就升级为轻量级锁,后续的同步操作就基于轻量级锁执行。

轻量级锁

  • 什么是轻量级锁

    轻量级锁就是避免了传统重量级锁的线程阻塞/唤醒等操作实现的线程同步,不过轻量级并不是替代重量级锁,在多线程竞争的条件下,仍然需要重量级锁进行同步,轻量级锁的目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统的互斥量产生的性能消耗。

  • 轻量级锁的加锁流程

    线程进入同步代码块,需要获取锁对象时,如果当前锁对象的锁标志位为“01”(未锁定、偏向锁模式的锁标志位相同),那么JVM会在当前的线程的栈帧中创建一个栈记录的空间,用于存储锁对象的Mark Word的拷贝。然后JVM使用CAS操作尝试将锁对象的Mark Word更新为指向当前线程Lock Record(栈记录)的指针,如果可以更新成功,那么表示获取到获取到轻量级锁,并将锁对象的锁标志位更新为“00”,表示此对象目前处于轻量级锁的状态。

    处于轻量级锁的优化为只是需要使用到轻量级的CAS操作,并不需要使用到系统内核的调度。

    如果CAS操作失败,那么表示当前有其他的线程在同时竞争获取该锁对象,因为CAS失败表示当前已经有其他的线程在同时进行CAS操作。JVM会检查当前锁对象Mark Word的指针是否指向当前线程的Lock Record,如果是,那么表示当前线程已经获取到锁,直接执行同步代码块中的代码。如果不是,表示锁对象被其他的线程抢占了。那么在多线程竞争的条件下,轻量级锁就无法作用,必须要膨胀为重量级锁,锁标志位修改为“10”,此时会将锁对象的Mark Word修改为指向重量级锁的指针(ObjectMonitor对象),并且等待锁的线程必须进入阻塞状态。

    当轻量级解锁的时候,会通过CAS操作将Lock Record中的内容和Mark Word中的指针互换,如果可以更新成功,那么轻量级锁正常解锁成功,如果CAS更新失败,那么表示有其他线程竞争过锁,并且锁对象已经膨胀为重量级锁,则在释放锁的同时,需要将阻塞的线程唤醒。

  • 轻量级锁升级

    在获取到轻量级锁时,如果CAS更新失败则会升级为重量级锁

    如果在获取轻量级锁时,CAS操作成功,则正常执行,表示获取到轻量级锁。

    如果CAS操作失败,那么表示当前有其他的线程在同时竞争获取该锁对象,因为CAS失败表示当前已经有其他的线程在同时进行CAS操作。JVM会检查当前锁对象Mark Word的指针是否指向当前线程的Lock Record,如果是,那么表示当前线程已经获取到锁,直接执行同步代码块中的代码。如果不是,表示锁对象被其他的线程抢占了。那么在多线程竞争的条件下,轻量级锁就无法作用,必须要膨胀为重量级锁,锁标志位修改为“10”,此时会将锁对象的Mark Word修改为指向重量级锁的指针(ObjectMonitor对象),并且等待锁的线程必须进入阻塞状态。

重量级锁

  • 重量级锁

    重量级锁在多线程竞争的条件下使用,通过操作系统内核进行线程的调度,当有一个线程获取到锁时,其他线程都会进行到阻塞状态,当线程释放锁时,会唤醒等待队列中的其他线程进行抢夺锁,因为涉及到内核态和用户态的转换,所以是非常重量级,耗费性能的操作。

锁升级流程

锁升级.png