简单说一说Synchronized的实现原理与应用

每次问到Synchroized都只会说,Synchroized可以

  • 同步一个普通方法,锁是当前实例对象

  • 同步一个静态方法,锁是当前Class的对象

  • 同步一个代码块,锁是括号里指定的对象

    完了嘛?就这?不能够哇!

    来,我们稍微往里探一探。


    实现原理

    我们先直接上图,一个是同步方法,一个是同步代码块,更直观来观察jvm是怎么来实现Synchronized的。

    1 简单说一说Synchronized的实现原理与应用_第1张图片

    2 简单说一说Synchronized的实现原理与应用_第2张图片

    ​ 我们用最原始的方法,用过CMD命令进入JAVA源文件目录下,使用javac xxx.java命令来编译源文件生成二进制的class文件。然后是使用javap -verbose xxx.class来查看详细的字节码文件,发现Synchronized同步代码块是通过识别monitorentermonitorexit指令来实现的。同步方法的时候,实在方法的字节码flag处通过ACC-SYNCHRONIZED来标记同步方法,这是Synchronized在JVM中实现同步代码块和方法两者之间的细节差异。

    每一个对象都有一个monitor对象与之关联,当一个monitor对象被持有时,他就会处于锁定状态,代码执行到monitorenter时就会去获取当前对象的monitor对象的所有权,既尝试获取对象的锁。也就可以这么去理解,每一个对象都有一个monitor对象,这个monitor是JVM中的对象,同时就是我们这个java对象的锁。

    同时,JVM保证了每一个monitorenter都有相应的monitorexit与之对应,加锁对应解锁。这是有人可能要问了,上面图里面3后面有一个monitorenter,1521后面有两个monitorexit啊,这是为什么呢?其实第二个monitorexit是JVM自动给我们加的,一般情况下,第一个monitorexit的后面goto跳转到24行是直接return的。第二个monitorexit是在程序异常的情况下,JVM主动帮我们去释放锁,避免死锁的情况发生。

    Synchronized优化

    虽然Synchronized是个老生常谈的东西,但是现实开发中貌似我们一直用的并不多,很大一部分原因可能大家还停留在Synchronized是个重量级锁的观念上。因为通过编译后的字节码,由系统来为我们操作加锁与解锁,涉及到Linux之间的用户态与内核态的状态切换,这是一个效率很低,并且开销很大的操作,所以一开始的Synchronized是很慢的。

    但是,早在JDK1.6中,就已经对Synchronized有过优化。简单说一说Synchronized的实现原理与应用_第3张图片想不到吧,现在都已经JDK14了!

    Java对象由三部分组成:对象头,实例数据,对齐填充。关键就在对象头里面,java对象头存储的东西还是比较多的,这里只讨论跟锁有关的。根据JDK1.6后锁的四种状态以及对象头Mark word中锁状态变化,大致如下:

    简单说一说Synchronized的实现原理与应用_第4张图片

    32位JVM的Mark word最后三位bit位,存储了偏向锁标记和锁标志位。其他还存有线程ID,hash,分代年龄等其他信息,详细可以阅读《深入理解JAVA虚拟机》。

    偏向锁

    ​ 初始无锁状态,一个线程进入同步块,会先检查对象头中是否存储了线程,如果没有的话则使用CAS去替换Mark word标志,同时将对象头中的线程ID指向自己,无锁立即升级为偏向锁。为什么会有偏向锁这个状态呢,因为经过测试发现,大多数情况下,锁总是由同一个线程去多次获取。为了减少内核态与用户态的切换带了的不必要消耗,当同一个线程再次获得锁的时候,只需要比较一下当前Mark word中持有的线程id,就可以避免无效的CAS操作来加锁与解锁。

    偏向锁的撤销,是在当有下一个线程进来竞争时,偏向锁升级。java6和java7中,默认开启偏向锁,同时我们也可以通过配置JVM参数关闭偏向锁延迟或者直接关闭偏向锁,关闭偏向锁后,默认进入轻量级锁。

    轻量锁

    ​ 存在偏向锁时,这时另一个线程进来获取锁,JVM会在当前线程中开辟一块栈帧区域用来存储锁记录,并将对象头中的Mark word 复制到锁记录中。然后,使用CAS来尝试将对对象头中的Mark word替换成指向锁记录的指针。假如说,此时上一个线程还为执行完毕,当前锁状态处于偏向锁,则当前线程开始自旋等待获取锁,锁立刻膨胀升级为轻量锁。从这个过程来看,轻量级锁的生命期可以近似的看成,对一个共享对象的加锁,每次都是在很短的时间内交替进行。锁的竞争不是很激烈,总是一个持有锁,一个等待锁,虽然会出现竞争,但是每次在锁自旋内,都能够成功获取锁。因为一旦出现“第三者”,有第三个线程进来说:不行,我也要,我也要抢。此时,锁膨胀升级为重量级锁。

    重量级

    ​ 重量级锁Mark word锁标志位更新成10,同时指向互斥量。这时候的实现就是上面所提到的monitor监视器对象来实现的了。我们的java代码,编译成计算器认识的字节码文件,操作系统底层通过Mutex Lock(互斥锁)来实现加锁。操作系统实现线程的切换经常需要从用户态切换到内核态,所谓的用户态、内核态简单理解就是,我们运行的系统,某些不影响系统稳运行的操作,可以开放操作权限由用户自己操作,这个状态就是用户态。某一些更细致的,安全性更高的操作,需要用户发送相应的指令去告诉计算机我想干什么,由系统去为我们实现,系统层面有自己的安全防控,总不能啥都让用户自己搞,万一崩了怎么办,这个状态下的操作就是内核态。这个成本是很高的,状态之间的切换很耗时,这也是为什么Synchronized一直被称为重量级锁的原因。

    其他一些小优化
    1. 锁消除

      • java在编译的时候,JVM虚拟机会对加锁的地方进行“逃逸分析”,如果认为一段逻辑,在堆上的数据不会出现资源竞争的情况,是线程安全的。即使你主动加了Synchronized锁,JVM也会进行相应的锁消除操作。
    2. 锁粗化

      • 当一个方法中有两个相邻的代码块,都使用了Synchronized加锁,JVM也会自动的进行锁粗化的优化,合并两个Synchronized的加锁,例如:

        public void add(){ synchronized(lock){ //TODO } /* 简单代码,执行速度快 */ synchronized(lock){ //TODO } }

        add方法中有两处Synchronized加锁,两处加锁之间又有一些执行速度快的代码段。这时候JVM就可以对整段add方法进行加锁,即加锁一次解锁一次,避免加锁两次解锁两次带来的状态切换消耗。

    如何应用

    ​ 说了这么多,我们到底什么时候去选择使用Synchronized呢?

    ​ Synchronized由JVM实现,自动加锁、解锁,异常情况自动处理不用担心死锁。但是一旦加锁开始后,就是不可中断的。所以在一些少量的同步场景,锁竞争不是很激烈,大致能判断竞争程度的情况下,还是可以用Synchronized来实现的,例如一些toB的业务场景中。相比JAVA中另一把锁Lock,现在的Synchronized并不是十分不堪,甚至在某些场景下效率还能够比Lock来的更高效,所以别再说Lock一定比Synchronized好了,不同的情况还是得不同考虑的。


    ​ 本文先简单的说一说Synchronized的实现,其实对于更加详细的加锁过程,比如monitor对象的构造,对于加锁维护了一个计数器是如何计数的,Synchronized释放锁之后是如何唤醒等待中的线程等等还是有很多值得探索的地方。但是一次性说太多也记不住,先了解一些基本的原理,引起自己兴趣才能由浅入深嘛,你说是不是呢!简单说一说Synchronized的实现原理与应用_第5张图片

    第一次写,如有不足,或是错误,欢迎评论指出!要是点个赞,那就更好了呢!

你可能感兴趣的:(Java,java,多线程)