偏向锁、轻量级锁、重量级锁、自旋锁原理讲解

一、简介

在讲解这些锁概念之前,我们要明确的是这些锁不等同于Java API中的ReentratLock这种锁,这些锁是概念上的,是JDK1.6中为了对synchronized同步关键字进行优化而产生的的锁机制。这些锁的启动和关闭策略可以通过设定JVM启动参数来设置,当然在一般情况下,使用JVM默认的策略就可以了。

二、Java对象头中的Mark Word

HotSpot中,Java的对象内存模型分为三部分,分别为对象头、实例数据和对齐填充。而对象头中分为两部分,一部分是“Mark Word”(存储对象自身的运行时数据,32bit或64bit,可复用);另一部分是指向它的类的元数据指针。

因为synchronized是Java对象的内置锁,所以其优化策略(即偏向锁等)的信息都包含在Mark Word中,让我们先看一下Mark Word的结构。

其中最重要的是“锁标志位”和“是否偏向锁”,锁标志位代表了当前对象内置锁的状态,不同的锁状态下Mark Word存储的信息是不同的,因此称为可复用。

三、偏向锁

通俗的讲,偏向锁就是在运行过程中,对象的锁偏向某个线程。即在开启偏向锁机制的情况下,某个线程获得锁,当该线程下次再想要获得锁时,不需要再获得锁(即忽略synchronized关键词),直接就可以执行同步代码,比较适合竞争较少的情况。

偏向锁的获取流程:

(1)查看Mark Word中偏向锁的标识以及锁标志位,若是否偏向锁为1且锁标志位为01,则该锁为可偏向状态。

(2)若为可偏向状态,则测试Mark Word中的线程ID是否与当前线程相同,若相同,则直接执行同步代码,否则进入下一步。

(3)当前线程通过CAS操作竞争锁,若竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码,若竞争失败,进入下一步。

(4)当前线程通过CAS竞争锁失败的情况下,说明有竞争。当到达全局安全点时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

偏向锁的释放流程:

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。

下面看一个实验:

/** *  * 开启偏向锁参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0  耗时4000ms左右 * 警用偏向锁参数:-XX:-UseBiasedLocking  耗时1900ms左右 */public class VectorTest {
    public static void main(String[] args) {        long time1 = System.currentTimeMillis();        Vector<Integer> vector = new Vector<Integer>();        for (int i = 0; i < 100000000; i++){            vector.add(100);//add是synchronized操作        }        System.out.println(System.currentTimeMillis() - time1);    }}

因为Vector是同步容器,其add操作是有synchronized修饰的,在JVM参数中设置开启偏向锁或禁用偏向锁时,其耗时相差很大,说明偏向锁对synchronized关键词的优化在单个线程中或竞争较少的线程中是很成功的。但是在多线程竞争十分频繁的情况下,偏向锁不仅不能提高效率,反而会因为不断地重新设置偏向线程ID等其他消耗而降低效率。

四、轻量级锁

轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。

轻量级锁的加锁过程:

(1)当线程执行代码进入同步块时,若Mark Word为无锁状态,虚拟机先在当前线程的栈帧中建立一个名为Lock Record的空间,用于存储当前对象的Mark Word的拷贝,官方称之为“Dispalced Mark Word”,此时状态如下图:

(2)复制对象头中的Mark Word到锁记录中。

(3)复制成功后,虚拟机将用CAS操作将对象的Mark Word更新为执行Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果更新成功,则执行4,否则执行5。;

(4)如果更新成功,则这个线程拥有了这个锁,并将锁标志设为00,表示处于轻量级锁状态,此时状态图:

(5)如果更新失败,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经拥有这个锁,可进入执行同步代码。否则说明多个线程竞争,轻量级锁就会膨胀为重量级锁,Mark Word中存储重量级锁(互斥锁)的指针,后面等待锁的线程也要进入阻塞状态。

五、重量级锁

即当有其他线程占用锁时,当前线程会进入阻塞状态。

六、自旋锁

在自旋状态下,当一个线程A尝试进入同步代码块,但是当前的锁已经被线程B占有时,线程A不进入阻塞状态,而是不停的空转,等待线程B释放锁。如果锁的线程能在很短时间内释放资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,只需自旋,等持有锁的线程释放后即可立即获取锁,避免了用户线程和内核的切换消耗。

自旋等待最大时间:线程自旋会消耗cpu,若自旋太久,则会让cpu做太多无用功,因此要设置自旋等待最大时间。

优点:开启自旋锁后能减少线程的阻塞,在对于锁的竞争不激烈且占用锁时间很短的代码块来说,能提升很大的性能,在这种情况下自旋的消耗小于线程阻塞挂起的消耗。

缺点:在线程竞争锁激烈,或持有锁的线程需要长时间执行同步代码块的情况下,使用自旋会使得cpu做的无用功太多。

JDK1.6中,设置参数 -XX:+UseSpinning开启。

JDK1.7后,由JVM自动控制。

原文发布于微信公众号 - Linyb极客之路(gh_c420b2cf6b47)

原文发表时间:2019-06-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券