synchronize底层实现原理以及相关的优化

首先来说下synchronize和Lock的区别:

两者都是锁,用来控制并发冲突,区别在于Lock是个接口,提供的功能更加丰富,除了这个外,他们还有如下区别:

  1. synchronize自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronize释放锁是由JVM自动执行的。
  2. Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入)
  3. Lock可以让线程在获取锁的过程中响应中断,而synchronize不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。
  4. Lock锁的是代码块,synchronize还能锁方法和类。
  5. Lock可以知道线程有没有拿到锁,而synchronize不能(Thread.holdsLock方法可以让synchronize也能感应到是否获取到了锁)

Lock锁对应有源码的,可以查看下代码,那么synchronize在JVM层面是怎么实现的呢,我们看下字节码文件:

  1. 先用javac Test.class 编译出class文件
  2. 再用javap –c Test.class查看字节码文件

我们写个DEMO看下,JVM底层是怎么实现synchronized的:、

public class Test4 {
 
    private static Object LOCK = new Object();
 
    public static int main(String[] args) {
        synchronized (LOCK){
            System.out.println("Hello World");
        }
        return 1;
    }
}

在看下上面代码对应的字节码
在这里插入图片描述

也就是说,锁是通过monitorentermonitorexit来实现的,这两个字节码代表的是啥意思:

可以在下面参考的网页中了解monitorenter和monitorexit的作用,我就不盗用他们的话了,大致意思是,每个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的。

我之前分析过一篇ReenternLock,概念都是类似的,只是锁是自身维护了一个volatile int类型的变量,通过对它加一减一表示占有锁啊重入之类的概念。

注意,如果synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增加这个一个标识符,获取它的monitor,所以本质上是一样的。

HotSpot中锁的具体实现以及对它的优化:

重量级锁:

最基础的实现方式,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程。阻塞和唤醒操作是依赖操作系统来完成的,所以需要从用户态切换到内核态,开销很大。并且monitor调用的是操作系统底层的互斥量(mutex),本身也有用户态和内核态的切换,所以JVM引入了自旋的概念,减少上面说的线程切换的成本

自旋锁:

如果锁被其他线程占用的时间很短,那么其他获取锁的线程只要稍微等一下就好了,没必要进行用户态和内核态之间的切换,等的状态就叫自旋。例如如下代码:

public class SpinLock {

    private AtomicReference<Thread> cas = new AtomicReference<Thread>();

    public void lock() {

        Thread current = Thread.currentThread();

        // 利用CAS,获取值不对则无限循环

        while (!cas.compareAndSet(null, current)) {

            // DO nothing

        }

    }

    public void unlock() {

        Thread current = Thread.currentThread();

        cas.compareAndSet(current, null);

    }

}

自旋会跑一些无用的CPU指令,所以会浪费处理器时间,如果锁被其他线程占用的时间段的话确实是合适的…如果长的话就不如使用直接阻塞了,那么JVM怎么知道锁被占用的时间到底是长还是短呢?

因为JVM不知道锁被占用的时间长短,所以使用的是自适应自旋。就是线程空循环的次数时会动态调整的

可以看出,自旋会导致不公平锁,不一定等待时间最长的线程会最先获取锁。

轻量级锁:

JDK1.6之后加入,它的目的并不是为了替换前面的重量级锁,而是在实际没有锁竞争的情况下,将申请互斥量这步也省掉。锁实现的核心在与对象头(MarkWord)的结构,对象自身会有信息表示所有被锁住并且锁是什么类型,如下所示:
在这里插入图片描述

如果代码进入同步块时,检测到对象未锁定,即标志位为01。那么当前线程就会在自身栈帧中建议一个区域保存对象的MarkWord信息,再使用CAS的方式让这个区域指向对象的MarkWork区域,这样就算加上锁了。(这样就没有获取系统mutex变量,只是改了个值,但是如果有竞争的话,就要升级成重量级锁,这样反倒变慢了)

加锁前VS 加锁后:

在这里插入图片描述

偏向锁:

比轻量级锁更绝,将同步操作全部省略…设置步骤是和前面的轻量级锁一样的,不同的是标志位设置的是01,即偏向模式。

不同的是同一个线程第二次进来之后,虚拟机不会再进行任何的同步操作,比如Mark Word的update。

如果有其他线程来,偏向模式就结束了,标志位会恢复到未锁定或者偏向锁。所以如果锁总是会被多个线程访问的话,还是禁止掉偏向锁优化比较好

锁优化流程如下:(出自周志明老师的那本讲解JVM的书)

在这里插入图片描述

可以看出,锁是一个逐步升级的过程,不会一开始上来就重量级锁。锁一般只会升级不会降级,避免降级之后冲突导致效率不行并且又得升级。但是降级其实是允许的(STW的时候),可以看下参考中文章里面提到的英文网站。

其他的优化还有锁消除以及锁粗化:

  1. 如果一段代码其实在作用域可以不加锁的,Javac编译器会自动优化。
  1. 锁粗化是指代码在一段代码中多次加锁,会被JVM优化成对整个代码段加锁。

(但是这两点是JVM对代码的优化,而不是对synchronized优化了,这里只是顺带提一下)

参考:

JVM规范官方文档
monotorenter/exit描述
CAS中的自旋锁
锁选用流程图
自旋锁的那两张图片
JVM锁降级

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值