Synchronized
synchronized的三种用法
-
修饰实例方法:以实例对象作为锁,进入同步代码前需要获得当前实例对象的锁
-
修饰类方法(static修饰的方法):以类对象为锁,进入同步代码块前需要获得当前类对象的锁
-
修饰代码块:需要指定一个锁对象
同步方法默认用 this 或者当前类 class 对象作为锁;同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。
对象头
对象由三部分组成----->对象头,实例数据和对齐填充。
synchronized关键字使用的锁对象(monitor)是存储在 Java对象头里的
对象头一部分是mark word,主要存放的是该对象的 hashcode值和锁的信息,另一部分是存放该对象的class metadata信息。在mark word中会用2个bit来记录锁的状态(偏向锁:01, 轻量级锁:00, 重量级锁:10)
原理
-
JVM 是通过进入、退出对象监视器(每个对象都存在着一个 monitor,monitor是由ObjectMonitor实现的,Java中任意对象可以作为锁的原因 )来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁 实现。具体实现是在编译之后在同步方法调用前加入一个monitorenter指令,在退出方法和异常处插入monitorexit的指令。
-
在同步块的入口和出口分别有monitorenter和monitorexit指令。当执行monitorenter指令时,当前线程将试图获取对象锁所对应的 monitor 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
优化
synchronized 核心优化方案主要包含以下 4 个:
-
锁膨胀:指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。
在最开始的时候,其实就是无锁直接到重量级锁,但是重量级锁需要向内核申请额外的锁资源,这就涉及到用户态和内核态的转换,比较浪费资源,而且大多数情况下,其实还是一个线程去争抢锁,完全不需要重量级锁.
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗, 引入了轻量级锁和偏向锁
-
锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
-
锁粗化:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。
eg.在 for 循环中定义锁,那么锁的范围很小,每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多
-
自适应自旋锁:自身循环,尝试获取锁,自适应是指,线程自旋的次数不是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。
通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。
锁膨胀(锁升级)
- 在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。