温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Java中synchronize底层的实现原理

发布时间:2021-05-17 11:31:59 来源:亿速云 阅读:381 作者:小新 栏目:编程语言

这篇文章给大家分享的是有关Java中synchronize底层的实现原理的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

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

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

  • synchronize自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronize释放锁是由JVM自动执行的。

  • Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入)

  • Lock可以让线程在获取锁的过程中响应中断,而synchronize不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。

  • Lock锁的是代码块,synchronize还能锁方法和类。

  • Lock可以知道线程有没有拿到锁,而synchronize不能

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

先用javac Test.class 编译出class文件再用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;
  }
}

在看下上面代码对应的字节码

Java中synchronize底层的实现原理

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

可以在下面参考的网页中了解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)的结构,对象自身会有信息表示所有被锁住并且锁是什么类型,如下所示:

Java中synchronize底层的实现原理

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

加锁前VS 加锁后:

Java中synchronize底层的实现原理

偏向锁:

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

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

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

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

Java中synchronize底层的实现原理

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

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

如果一段代码其实在作用域可以不加锁的,Javac编译器会自动优化。

锁粗化是指代码在一段代码中多次加锁,会被JVM优化成对整个代码段加锁。

Java有哪些集合类

Java中的集合主要分为四类:1、List列表:有序的,可重复的;2、Queue队列:有序,可重复的;3、Set集合:不可重复;4、Map映射:无序,键唯一,值不唯一。

感谢各位的阅读!关于“Java中synchronize底层的实现原理”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI