JUC学习:synchronized与锁升级介绍及原理

        

目录

1 线程加锁流程

2 对象头锁状态对照

2.1 无锁态        

2.2 偏向锁

2.3 轻量级锁

 2.4 重量级锁

3 各锁的优缺点对比

3.1 偏向锁

3.2 轻量级锁

3.3 重量级锁


        在高并发的情况下为保证线程安全需要加锁,但也要考虑性能问题,所以能无锁就不要加锁,能锁区块代码(同步代码块)就不要在整个方法上加锁,能使用对象锁就不要使用类锁。

        锁的升级过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

1 线程加锁流程

        在对象的内存布局中,对象头包含Mark Word(对象标记),当加的是重量级锁(synchronized)时包含一个指向对象监视器(ObjectMonitor,即管程)的指针 (可查看《JUC学习:对象内存布局》部分内容)。对象监视器(ObjectMonitor,即管程)中又包含如下参数:

参数含义
_owner该对象锁定的线程id
_count初始值0,每用该对象加一次锁+1,解锁-1(可重入锁部分需要)
_recursions初始值0,表示代码块重入次数,每重入一层+1,出一层-1
_EntryList阻塞队列,存放阻塞的线程
_WaitSet等待队列,存放等待的线程

        当对象(obj)被线程(t1)作为锁时,会将(obj)指向的ObjectMonitor的 _owner 参数赋值为线程(t1)的id、将其 _count 加1,并将 _recursions 参数加1,此时(t1)线程得到锁。

        当线程(t2)想到加锁时,发现对象(obj)的 _count 不等于0,即对象(obj)已被线程占用。再判断对象(obj)锁的线程是不是(t2)。

  • 若是(t2)则由于锁的可重入性,则执行加锁操作(将其 _count 、_recursions 参数加1);
  • 若不是(t2),则代表对象(obj)已被其它线程获取,则(t2)线程会进入阻塞队列_EntryList中,等待(t1)线程释放锁,再次进行抢夺锁的操作。

流程如下图所示:

         这是一个非常重量级的操作,涉及到代码,虚拟机直至底层操作系统的各种状态修改。在高并发的情况下非常影响程序的性能,因此后续引入了偏向锁以及轻量级锁。

2 对象头锁状态对照

        对象锁升级的流程就是锁标记的变化。从无锁 到 偏向锁 到 轻量级锁 到 重量级锁。

锁定

2.1 无锁态        

        一个对象没有被任何线程当作锁竞争,则该对象就是无锁态。

        从上图中可看出,在无锁状态下,前25位无数据,31位hashcode,后三位锁标记,通过如下代码举例验证。从输出结果图可以看出在无锁态下与上表中的信息对照一致。

public class SyncUp {

    public static void main(String[] args) {

        Object o = new Object();

        System.out.println("10进制" + o.hashCode());
        System.out.println("16进制" + Integer.toHexString(o.hashCode()));
        System.out.println("2进制" + Integer.toBinaryString(o.hashCode()));

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

2.2 偏向锁

        在多线程竞争锁的情况下,大多数情况下会发生同一个线程持续抢夺到锁的情况(即a,b,c三个线程,a线程每次都能抢到锁)。在这种情况下,为了避免在每次加锁的过程中大量的修改底层状态(内核态到用户态的转换)带来的性能下降,则为线程加上了偏向锁。

        偏向锁会偏向第一个访问锁的线程,若后续没有其它线程访问该锁(即该锁一直被同一个线程获取),则持有偏向锁的线程永远不会触发同步机制(即直接消除了同步语句),避免了底层大量状态的修改,直接提升了系统性能,详细流程如下。

        锁(对象obj)第一次被线程(t1)拥有时,锁(对象obj)的对象头中会记录这个锁(obj)是一个偏向锁(即锁标记是101),并在对象头中记录54位的指向线程(t1)的指针。则线程(t1)在下次访问该同步代码块时,不需要再加锁与释放锁,而是直接判断锁(obj)的54位指针是否指向的是(t1)线程。

  • 是当前线程,则无需再与其他线程竞争锁,同理之后每次都只需要检查锁(obj)中记录的线程指针即可,省略了加锁、释放锁的操作,大大提升了系统性能。
  • 不是当前线程,代表发生了竞争,会尝试使用CAS操作来替换锁(obj)对象头中的线程指针。

        竞争可能成功也可能失败。

  • 竞争成功,代表线程(t1)已经执行完成不存在了,那么会执行CAS操作,将锁(obj)对象中指向(t1)的指针修改为指向(t2)的指针,代表(t2)线程竞争成功。
  • 竞争失败,代表线程(t1)还存在,并与线程(t2)发生了竞争,此时则会触发锁升级操作,升级位轻量级锁,保证线程间的公平竞争。

注意:线程不会主动释放偏向锁,只有遇到其它线程尝试竞争锁时,拥有偏向锁的线程才会被动的释放偏向锁。

        以下代码用于测试偏向锁,在java虚拟机中,偏向锁默认会有4000ms的开启延时。为了便于测试需要添加-XX:BiasedLockingStartupDelay=0 jvm参数,取消开启延时。

//jvm参数-XX:BiasedLockingStartupDelay=0
public class BiasedLockDemo {
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

        通过对象头的Mark Word(对象标记)后3位101可看出该Object对象是一个偏向锁。

2.3 轻量级锁

        在单线程运行的偏向锁模式下,此时若有其他线程来竞争锁(通过CAS操作修改对象头中的锁指针),在修改成功的情况新线程则继续使用偏向锁;修改失败时代表线程之间存在竞争,则会 进行锁升级策略,将锁从偏向锁升级为轻量级锁。

        在轻量级锁模式下,每个线程通过CAS争夺锁,争夺成功则进入同步代码块,争夺失败则进行自旋操作直至抢夺到锁为止。

        轻量级锁适合在线程竞争不是很激烈的情况下使用,当线程数过多,可能会出现线程长时间自旋的问题。在jdk6之前,设定的轻量级锁升级策略是在自旋10次后,或是自旋数超过了cpu核数的一半时升级为重量级锁;jdk6之后这个数量是动态的、不固定的,上一次自旋抢夺到之后这个数量会增大,上一次自旋没有抢夺到会减少。

//轻量级锁,使用jvm参数-XX:-UseBiasedLocking关闭偏向锁,则直接从无锁态升级到轻量级锁。
public class LightweightLock {
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

        通过对象头的Mark Word(对象标记)后2位00可看出该Object对象是一个轻量级锁。

 2.4 重量级锁

        重量级锁加锁流程参考第一部分synchronized线程加锁流程。

public class HeavyweightLock {
    public static void main(String[] args) {
        Object o = new Object();
        Object o2 = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }, "t2").start();
    }
}

        通过对象头的Mark Word(对象标记)后2位10可看出该Object对象是一个轻量级锁。 

3 各锁的优缺点对比

3.1 偏向锁

  • 优点:加锁与解锁不需要额外消耗,和执行非同步代码块仅存在纳秒级差距
  • 缺点:如果线程间存在锁竞争,会带来额外锁撤销的消耗。
  • 适用场景:单线程执行同步代码块的场景。

3.2 轻量级锁

  • 优点:竞争线程不会阻塞(CAS)提高了程序的响应速度。
  • 缺点:如果线程长时间竞争不到锁,一直自旋会消耗cpu性能。
  • 适用场景:追求响应时间,同步代码块执行速度非常快。

3.3 重量级锁

  • 优点:线程竞争不使用自旋,不会消耗cpu资源。
  • 缺点:得不到锁得线程会阻塞,加锁解锁较为复杂会导致程序响应缓慢。
  • 适用场景:追求吞吐量,同步代码块执行速度较长。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值