目录
- synchronized基本使用
- synchronized底层原理
-
- Monitor
- Mark Word
- synchronized锁优化
-
- 偏向锁
- 轻量级锁
- 自旋锁
- 适应性自旋锁
- 锁消除
- 锁粗化
synchronized基本使用
synchronized可以作用在方法、静态方法、类、代码块上
一、修饰方法,修饰方法时锁的范围是整个方法,锁的对象是调用该方法的实例对象(不同实例对象互不影响),需要注意的是synchronized不能被子类重写的方法继承
public synchronized void method() {
//TODO
}
二、修饰一个代码块,如果代码块包住了整个方法的代码则等价于修饰了该方法,修饰代码块时必须指定一个对象,这个对象可以是this也可以new一个对象,获取锁时是获取了这个对象的锁
public void method() {
synchronized(this) {
//TODO
}
}
Object obj = new Object();
public void method() {
synchronized(obj) {
//TODO
}
}
三、修饰一个静态方法,修饰在静态方法上时,锁作用的对象是这个类的对象
public synchronized static void method() {
//TODO
}
四、修饰一个类,这种写法和修饰静态方法的效果是一样的,都是锁住了这个类的对象
class Demo {
public void method() {
synchronized(Demo.class) {
//TODO
}
}
}
synchronized底层原理
Monitor
synchronized基于对象的Monitor锁(监视器锁)实现,每个对象都持有Monitor,线程通过Monitorenter指令进入Monitor持有锁,通过Monitroexit指令退出Monitor对象释放锁,Monitor锁底层是操作系统通过Mutex lock实现的,所以如果直接操作Monitor就会涉及到用户态和内核态的转换,非常消耗性能,因此synchronized被称为重量级锁,当然在Java1.5版本之后Oracle公司已经对synchronized做了大量优化,性能和ReentrantLock持平,反编译字节码文件后我们会发现当synchronized修饰在方法上时是使用一个标识符ACC_SYNCHRONIZED来标识当前方法是一个同步方法,而当synchronoized修饰代码块时使用了一次monitorenter指令和两次monitorexit指令,第一次是同步正常退出释放锁,第二次是发生异步退出释放锁
Mark Word
每个对象都持有一个Monitor,那么对象是如何记录Monitro信息的呢,答案就是Mark Word(对象头),内存中的对象由三部分组成:对象头、实例数据、对齐填充
- 对象头:包含对象的hashcode、对象所属的年代、锁标记位、偏向锁(线程)ID、偏向时间、数组长度等,对象头一般占用2个机器码(子宽),32位虚拟机中一个机器码是4个字节(32bit),64位虚拟机中一个机器码是8个字节(64bit),如果对象是个数组则会占用3个机器码,其中一个机器码用来记录数组长度
- 实例数据:存放类的属性数据信息,包括父类的属性信息
- 对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍,填充数据只是为了字节对齐
为了存储更多的数据,Mark Word被设计成一个非固定的结构,它会根据对象的状态复用自己的存储空间,也就是说Mark Word是可变的,变化的结构如图:
synchronized锁优化
synchronized锁优化后有四种状态,级别从低至高分别是:无锁状态、偏向锁、轻量级锁、重量级锁,随着锁竞争的激烈,锁的级别是逐渐上升的,叫做锁的升级,锁的升级是单向的只能升级不能降级
偏向锁
偏向锁是JDK1.6后新引入的,研究发现在大多数情况下,锁不仅不存在多线程竞争,而且总是由一个线程多次获取,因此为了减少线程每次获取锁的耗时而引入了偏向锁,偏向锁的核心思想是当线程获取锁之后,锁进入偏向模式,当线程再次获取锁时无需CAS操作,从而减少消耗提高性能,偏向锁适合单线程执行代码块的场合,在锁竞争激烈的场合则会升级为轻量级锁或重量级锁,偏向锁默认是开启的,可以通过参数-XX:-UseBiasedLocking禁止偏向锁
轻量级锁
偏向失败后,JVM先尝试升降到轻量级锁,使用轻量级锁是为了减少直接使用重量级锁引起的开销,轻量级锁提升性能的依据是"对于绝大部分的锁,在整个生命周期内是不会产生竞争的",轻量级锁适用于多个线程交替执行代码块的场合,如果线程之间的竞争更加激烈,则从轻量级锁膨胀为重量级锁
自旋锁
由于在大多数情况下线程持有锁的时间不会太长,为了避免在短时间内阻塞和唤起线程,引入了自旋的概念,当一个线程尝试获取锁时,如果该锁已经被其他线程占据,则进入空循环等待锁释放,而非进入休眠,这个过程叫做自旋,当然自旋会持续占用CPU,如果锁长时间不释放,就会白白浪费性能,因此自旋的时机或者次数需要有个上限,如果长时间获取不到锁,则升级到重量级锁
适应性自旋锁
如果某次自旋成功了,那么下次自旋的次数将会增加,反之如果自旋很少成功,则自旋次数会减少,也就是说JVM会根据上次自旋的情况调整自旋的次数
锁消除
锁消除指的是虚拟机在编译时会去除不可能存在共享资源竞争的锁,节省无意义的加锁时间,有点时候并非我们主动写的同步代码而是使用的api中具有同步方法比如stringBuff的append方法,锁消除的依据是逃逸分析的数据支持,使用JVM参数可控制是否开启逃逸分析,默认是开启的,-XX:+DoEscapeAnalysis : 开启逃逸分析 -XX:-DoEscapeAnalysis : 关闭逃逸分析
锁粗化
锁粗化也很好理解,实际在使用synchronized时,我们可能会隔一段代码使用一个synchronized同步块,这种情况下连续加锁会造成额外的损耗,JVM会将一段一段的锁合并成一个锁