> 技术文档 > Java中synchronized的神秘面纱

Java中synchronized的神秘面纱


关于synchronized你了解多少?

 讲讲synchronized?synchronized和ReentrantLock,Locaksupport的区别?

synchronized的实现原理?synchronized发生异常会死锁吗?为什么?

了解JMM吗?synchronized怎么保证的可见性和有序性?

什么是对象锁,什么是类锁?什么是synchronized锁升级?

以上是面试中经常被问到的问题,你真的了解synchronized吗?这篇文章我们来好好总结一下。

基本原理

  synchronized是Java语言中的一个关键字,相信有点儿java基础的同学都或多或少的了解过。

synchronized可以修饰普通方法,静态方法,代码块。在《阿里巴巴Java开发手册》中有提到,能修饰代码块,就不要锁整个方法。能使用对象锁 就不要使用类锁。纳尼》?什么是对象锁什么是类锁?相信读完下面的代码你就会有所了解。

读者不妨读一下上面这段代码,你觉得这段代码的执行结果应该是什么?

“肯定是先打印 发送邮件啊,因为打印短信的线程睡眠了2000毫秒。”

答案是先打印 发送短信 ,再打印 发送邮件。那么为什么呢?

当synchronized修饰的是普通方法时,锁的是“this”,也就是这个锁对象。不是单纯的那一个方法 ,为什么锁的是一个对象呢?往下看直到看完下面的对象内存布局你或许就能理解了。

接着再看一个案例

 

我加了一个hello方法,大家觉得会先打印什么呢》?答案是先打印hello,因为锁住的只是锁对象的临界资源,你可以理解为只锁的是带有synchronized的代码。

那么,趁热打铁,再看两个案例。

 

我现在换成了两个对象,大家应该很容易猜到先打印啥吧?当然是先打印发送邮件了。对象锁只针对一个对象。那么,你再看下一个案例呢

代码看起来好像没变,我是在Phone类里的两个同步方法加了static修饰,大家觉得这样会先打印哪个呢》》?答案是先打印 发送短信。为什么呢?因为之前我们synchronized只是修饰的普通方法,当synchronized修饰的是静态方法时,锁的就不是对象了,锁的是整个类,也就是以这个类为模板的所有对象。这就是所说的类锁。

为什么synchronized不跟Lock一样需要手动解锁呢?

相信看完上面的内容你对对象锁和类锁有了一定的了解,你有没有发现,使用synchronized编写代码的时候不需要我们手动解锁,使用Reentantlock的时候我们需要手动解锁,还需要放到finally代码块中防止代码发生异常。Locksupport也类似,虽然不用加锁解锁成对出现,但也有一种类似许可证的方式进行解锁。让我们来看一下synchronized的字节码指令(笔者这里用的工具是jclasslib)。

这是代码,然后我们编译后(我这里使用的是Runtime类提供的方法)使用jclasslib打开字节码文件会看到

有一个monitorenter指令和两个monitorexit指令。仔细看,从第16 aload开始是发生异常走的指令。这里有一个monitorexit然后下面19有个athrow。这说明发生异常后synchronized是先解锁,然后抛出异常。

synchronized怎么保证的可见性和有序性?

上面我们说到,monitorenter是加锁的指令,monitorexit是解锁的指令。这里我们先说一下什么是可见性和有序性,可见性和有序性是java内存模型JMM围绕并发过程处理的两个主要特性(还有一个是原子性,这里我就不展开了,毕竟我们都知道,synchronized保证原子性),可见性 是指一个线程更改某个共享变量的值时其他线程会立刻得知修改,有序性泛指指令的重排序。我们来说说上面的两个指令就不难理解了。当代码进入同步块中时也就是执行monitorenter指令时,会插入一个加载屏障(内存屏障的一种,其实有四种,这里不用分那么细),加载屏障会触发缓存一致性协议(如MESI协议),强制去主内存(不懂主内存和工作内存是什么的可以去看看JMM)读取变量值而不是自己工作内存的值,然后屏障(适用于这两个屏障)的另一个作用是防止屏障后面的指令重排序到屏障之前。解锁时(monitorexit)会插入一个存储屏障,强制将更新的变量值刷新回主内存中。这就保证了可见性和有序性。

为什么任何一个对象都可以成为一把锁?

相信大家用synchronized写同步代码块时,都会在括号里面随便写一个对象。比如:

那么,为什么任何一个对象都可以成为一个锁,这也是面试经常问的一个问题。在网上搜或者问AI大模型之类的可能会得到一个答案:这是因为每个对象都会有一个monitor对象与之隐式关联。

我当时读这句话的时候也是非常不理解,啥是monitor对象?隐式?(注意不要跟后面的ObjectMonitor对象弄混了),其实吧,monitor对象不是真实存在的,这只是抽象出来的一种概念,就跟JVM模型中的方法区似的,是逻辑规范中抽象出来的,并不真实存在。monitor的实现是由对象实例中的对象头的MarkWord的偏向标志位和锁标志位实现的。(这里不明白的话可以看一下对象内存布局)MarkWord中会有1bit的偏向标志位和2bit的锁标志位,每个对象中都有,所以每个对象都可以成为一把锁。

什么是synchronized锁升级?

不知不觉已经到了锁升级了,了解锁升级的话首先要了解对象的内存布局。

这里借用一下别的博主的图。我们看到锁标志位是用来描述锁的状态的。01代表无锁或者偏向锁(前面的偏向标志为1),00代表轻量级锁,10代表重量级锁,11是GC标记。

锁升级过程

当没有其他线程竞争时,只有一个线程进入锁,此时锁就会把偏向标志位改为1,代表现在是偏向锁模式,偏向锁(jdk1.6引入)的目的是在没有线程竞争时提高程序的性能,以后这个线程再进入代码块时虚拟机不会再进行任何的同步操作。

当有另一个线程试图去获取这把锁时,此时发生竞争了。偏向模式宣告结束,撤销偏向标志位,锁标志位置为00,升级为轻量级锁。那么,什么是轻量级锁?

我们知道,当有很多线程去竞争锁的时候,只会有一个线程成功进入,其他线程只能阻塞。但是阻塞线程的权限操作系统只有内核态才有,所以要阻塞线程就要让操作系统从用户态转换为内核态,但是在竞争不激烈的情况下这样做开销太大,所以,我们有了轻量级锁,其他线程不是阻塞了,而是采用cas自旋的方式一直空转(cas是一种无锁机制,搭配while(true)会让线程一直执行),而不会让操作系统转换为内核态。

但是当cas自旋次数过多或者竞争太激烈时,jvm会将对象头的锁标志位换为重量级锁10,然后会创建一个ObjectMonitor的实例来协调管理这些线程。

ObjectMonitor的内部结构主要包括以下几个部分:

  • Header(头部信息):存储锁的状态信息,例如锁的类型(偏向锁、轻量级锁、重量级锁)、锁的持有者(线程ID)等。

  • Wait Set(等待集):一个队列,存储所有调用了wait方法而被挂起的线程。

  • Entry Set(入口队列):一个队列,存储所有尝试获取锁但未能获取的线程。

  • Owner(锁的持有者):当前持有锁的线程。

  • Count(锁的计数器):记录锁的重入次数(例如,一个线程可以多次获取同一个锁)。

香烟价格