死磕synchronized底层原理

简介: 详细讲解synchronized关键字~

文章已收录Github精选,欢迎Starhttps://github.com/yehongzhi

前言

作为Java程序员,我们都知道在多线程的情况下,为了保证线程安全,经常会使用synchronized和Lock锁。Lock锁之前写过一篇《不得不学的AQS》,已经详细讲解过Lock锁的底层原理。这次我们讲一下日常开发中常用的关键字synchronized,想要用得好,底层原理必须要搞明白。

synchronized是JDK自带的一个关键字,在JDK1.5之前是一个重量级锁,所以从性能上考虑大部分人会选择Lock锁,不过毕竟是JDK自带的关键字,所以在JDK1.6后对它进行优化,引入了偏向锁,轻量级锁,自旋锁等概念。

一、synchronized的使用方式

在语法上,要使用synchronized关键字,需要把任意一个非null对象作为"锁"对象,也就是需要一个对象监视器(Object Monitor)。总的来说有三种用法:

1.1 作用在实例方法

修饰实例方法,相当于对当前实例对象this加锁,this作为对象监视器。

public synchronized void hello(){
    System.out.println("hello world");
}

1.2 作用在静态方法

修饰静态方法,相当于对当前类的Class对象加锁,当前类的Class对象作为对象监视器。

public synchronized static void helloStatic(){
    System.out.println("hello world static");
}

1.3 修饰代码块

指定加锁对象,对给定对象加锁,括号括起来的对象就是对象监视器。

public void test(){
    SynchronizedTest test = new SynchronizedTest();        
    synchronized (test){
        System.out.println("hello world");
    }
}

二、synchronized锁的原理

在讲原理前,我们先讲一下Java对象的构成。在JVM中,对象在内存中分为三块区域:对象头,实例数据和对齐填充。如图所示:
在这里插入图片描述
对象头

  • Mark Word,用于存储对象自身运行时的数据,如哈希码(Hash Code),GC分代年龄,锁状态标志,偏向线程ID、偏向时间戳等信息,它会根据对象的状态复用自己的存储空间。它是实现轻量级锁和偏向锁的关键
  • 类型指针,对象会指向它的类的元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
  • Array length,如果对象是一个数组,还必须记录数组长度的数据。

实例数据

  • 存放类的属性数据信息,包括父类的属性信息。

对齐填充

  • 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

2.1 同步代码块原理

为了看底层实现原理,使用javap -v xxx.class命令进行反编译。
在这里插入图片描述
这是使用同步代码块被标志的地方就是刚刚提到的对象头,它会关联一个monitor对象,也就是括号括起来的对象。

1、monitorenter,如果当前monitor的进入数为0时,线程就会进入monitor,并且把进入数+1,那么该线程就是monitor的拥有者(owner)。

2、如果该线程已经是monitor的拥有者,又重新进入,就会把进入数再次+1。也就是可重入的。

3、monitorexit,执行monitorexit的线程必须是monitor的拥有者,指令执行后,monitor的进入数减1,如果减1后进入数为0,则该线程会退出monitor。其他被阻塞的线程就可以尝试去获取monitor的所有权。

monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

总的来说,synchronized的底层原理是通过monitor对象来完成的。

2.2 同步方法原理

比如说使用synchronized修饰的实例方法。

public synchronized void hello(){
    System.out.println("hello world");
}

同理使用javap -v反编译。
在这里插入图片描述
可以看到多了一个标志位ACC_SYNCHRONIZED,作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有这个标志位,就会先尝试获取monitor,获取成功才能执行方法,方法执行完成后再释放monitor。在方法执行期间,其他线程都无法获取同一个monitor。归根结底还是对monitor对象的争夺,只是同步方法是一种隐式的方式来实现。

2.3 Monitor

上面经常提到monitor,它内置在每一个对象中,任何一个对象都有一个monitor与之关联,synchronized在JVM里的实现就是基于进入和退出monitor来实现的,底层则是通过成对的MonitorEnter和MonitorExit指令来实现,因此每一个Java对象都有成为Monitor的潜质。所以我们可以理解monitor是一个同步工具。

三、synchronized锁的优化

前面讲过JDK1.5之前,synchronized是属于重量级锁,重量级需要依赖于底层操作系统的Mutex Lock实现,然后操作系统需要切换用户态和内核态,这种切换的消耗非常大,所以性能相对来说并不好。

既然我们都知道性能不好,JDK的开发人员肯定也是知道的,于是在JDK1.6后开始对synchronized进行优化,增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。锁的等级从无锁,偏向锁,轻量级锁,重量级锁逐步升级,并且是单向的,不会出现锁的降级。

3.1 自适应性自旋锁

在说自适应自旋锁之前,先讲自旋锁。上面已经讲过,当线程没有获得monitor对象的所有权时,就会进入阻塞,当持有锁的线程释放了锁,当前线程才可以再去竞争锁,但是如果按照这样的规则,就会浪费大量的性能在阻塞和唤醒的切换上,特别是线程占用锁的时间很短的话。

为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。

但是又有问题,由于线程是一直在循环检测锁的状态,就会占用cpu资源,如果线程占用锁的时间比较长,那么自旋的次数就会变多,占用cpu时间变长导致性能变差,当然我们也可以通过参数-XX:PreBlockSpin设置自旋锁的自旋次数,当自旋一定的次数(时间)后就挂起,但是设置的自旋次数是多少比较合适呢?

如果设置次数少了或者多了都会导致性能受到影响,而且占用锁的时间在业务高峰期和正常时期也有区别,所以在JDK1.6引入了自适应性自旋锁。

自适应性自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

表现是如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能最大化利用资源,随着程序运行和性能监控信息的不断完善,虚拟机对锁的状况预测会越来越准确,也就变得越来越智能。

3.2 锁消除

锁消除是一种锁的优化策略,这种优化更加彻底,在JVM编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。这种优化策略可以消除没有必要的锁,节省毫无意义的请求锁时间。比如StringBuffer的append()方法,就是使用synchronized进行加锁的。

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

如果在实例方法中StringBuffer作为局部变量使用append()方法,StringBuffer是不可能存在共享资源竞争的,因此会自动将其锁消除。例如:

public String add(String s1, String s2) {
    //sb属于不可能共享的资源,JVM会自动消除内部的锁
    StringBuffer sb = new StringBuffer();
    sb.append(s1).append(s2);
    return sb.toString();
}

3.3 锁粗化

如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。意思是将多个连续加锁、解锁的操作连接在一起,扩展成为一个范围更大的锁。

3.4 偏向锁

偏向锁是JDK1.6引入的一个重要的概念,JDK的开发人员经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。也就是说在很多时候我们是假设有多线程的场景,但是实际上却是单线程的。所以偏向锁是在单线程执行代码块时使用的机制。

原理是什么呢,我们前面提到锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。

一旦有多个线程开始竞争锁的话呢?那么偏向锁并不会一下子升级为重量级锁,而是先升级为轻量级锁。

3.5 轻量级锁

如果获取偏向锁失败,也就是有多个线程竞争锁的话,就会升级为JDK1.6引入的轻量级锁,Mark Word 的结构也变为轻量级锁的结构。

执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。

自旋锁的原理在上面已经讲过了,如果自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。

3.6 重量级锁

重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。

3.7 小结

偏向锁:适用于单线程执行。

轻量级锁:适用于锁竞争较不激烈的情况。

重量级锁:适用于锁竞争激烈的情况。

四、Lock锁与synchronized

我们看一下他们的区别:

  • synchronized是Java语法的一个关键字,加锁的过程是在JVM底层进行。Lock是一个类,是JDK应用层面的,在JUC包里有丰富的API。
  • synchronized在加锁和解锁操作上都是自动完成的,Lock锁需要我们手动加锁和解锁。
  • Lock锁有丰富的API能知道线程是否获取锁成功,而synchronized不能。
  • synchronized能修饰方法和代码块,Lock锁只能锁住代码块。
  • Lock锁有丰富的API,可根据不同的场景,在使用上更加灵活。
  • synchronized是非公平锁,而Lock锁既有非公平锁也有公平锁,可以由开发者通过参数控制。

个人觉得在锁竞争不是很激烈的场景,使用synchronized,语义清晰,实现简单,JDK1.6后引入了偏向锁,轻量级锁等概念后,性能也能保证。而在锁竞争激烈,复杂的场景下,则使用Lock锁会更灵活一点,性能也较稳定。

总结

学习synchronized关键字的底层原理不是钻牛角尖,其实是从底层原理上知道了synchronized在什么场景使用会有什么样的效果,我们都知道没有最好的技术,只有最适合的技术,所以在学完之后,希望对大家有所帮助,写出更加高效的代码。所谓不积跬步无以至千里,一步一个脚印,哪怕现在还是菜鸟,总有一天也会成为雄鹰。

那么这篇文章就写到这里了,感谢大家的阅读,希望看完后能有所收获!

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!
在这里插入图片描述

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

相关文章
|
3月前
|
存储 Java 调度
死磕-java并发编程技术(一)
死磕-java并发编程技术(一)
|
3月前
|
Java
死磕-java并发编程技术(二)
死磕-java并发编程技术(二)
|
2月前
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
|
5月前
|
存储 安全 Java
(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现
关于源码分析如果不是功底特别深厚的小伙伴可能需要用心的去细心咀嚼,千万不要抱着看一遍就能懂的心态学习,不然最终也没有任何作用。
|
6月前
|
安全 Java 开发者
一文弄懂synchronized
一文弄懂synchronized
79 0
|
Java C++
说一下 synchronized 底层实现原理?(高薪常问)
说一下 synchronized 底层实现原理?(高薪常问)
113 2
|
存储 安全 Java
死磕synchronized二:系统剖析延迟偏向篇一
近期准备写一个专栏:从Hotspot源码角度剖析synchronized。前前后后大概有10篇,会全网发,写完后整理成电子书放公众号供大家下载。对本专栏感兴趣的、希望彻彻底底学明白synchronized的小伙伴可以关注一波。电子书整理好了会通过公众号群发告知大家。我的公众号:**硬核子牙**。
288 0
死磕synchronized二:系统剖析延迟偏向篇一
『死磕Java并发编程系列』并发编程工具类之CountDownLatch
『死磕Java并发编程系列』并发编程工具类之CountDownLatch
『死磕Java并发编程系列』并发编程工具类之CountDownLatch
|
安全
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)
202 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)
|
缓存 Oracle Java
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
136 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)