多线程并发编程中 synchronized 一直是元老级别的角色。在java se1.6 以后为了减少获得锁和释放锁带来的性能消耗引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程。
1、 java 中每一个对象都有可以作为锁,具体表现为:
1)对于普通同步方法,锁是当前的实例对象。
2)对于静态同步方法,锁是当前类的Class对象
3)对于同步方法块,锁是 synchronized 括号里配置的对象。
2、同步机制
JVM 基于进入和退出Monitor 对象来实现方法同步和代码块同步,两者实现的细节不一样。代码块同步是 使用 monitorenter 和 monitorexit 指令实现的。方法的同步是另外一种方式实现的,但是方法的同步同样可以通过这两个指令实现。
monitorenter 指令是编译后插入到同步代码块的开始位置。
monitorexit 指令是插入到方法结束处和异常处
JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之相配对。任何一个对象都有一个 monitor 与之关联,且当一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的monitor 的所有权,既尝试获取对象的锁。
3、synchronized 的存储。
synchronized 是存储在对象头中的,若果对象是数组类型,则虚拟机用 3个字宽存储对象头,若对象是非数组类型,则虚拟机用2个字宽存储对象头。在 32位虚拟机下,1字宽等于4字节,32bit。
长度 | 内容 | 说明 |
32/64 bit | Mark Word | 存储对象的 hashCode 或者锁信息 |
32/64 bit | Class Metadatan Address | 存储到对象类型数据的指针 |
32/64 bit | Array length | 数组的长度(如果当前对象是数组) |
32位虚拟机中Mark Word 的存储:
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
轻量级所 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级索)的指针 | 10 | |||
GC 标记 | 空 | 11 | |||
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
64位虚拟机中Mark Word 的存储:
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
cms_free | 分代年龄 | 偏向锁 | 锁标志位 | |||
无锁 | unused | hashCode | 0 | 01 | ||
偏向锁 | ThreadID(54bit) Epoch(2bit) | 1 | 01 |
4、synchronized 锁的升级
在 JAVA SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”。因此,有四种状态的锁,由低到高依次为:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。这几个状态会随着竞争情况逐渐升级,可以升不能降级。
1) 偏向锁
在大多数情况下,锁不存在多线程竞争,而是由一个线程多次获得,因此为了获得的代价更低便有了偏向锁。在线程的对象头和栈帧中的锁记录中记录锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁。
偏向锁在 java6和7 中默认是开启的,但是会在几秒之后才会激活。可以设置参数 -XX:BiasedLockingStartupDelay=0 关闭延迟。
若是确定 所有锁都是竞争状态,可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false 程序会进入轻量级锁状态。
2)轻量级锁
线程在同步代码块之前,JVM会在当前线程的栈帧中创建用于锁记录的空间,并且将对象头中的Mark Word 复制到锁记录中。线程尝试CAS将对象对象头中的MarkWord替换为指向锁记录的指针。成功获得锁,失败尝试自旋获取锁。自旋获取失败变为重量级索。
解锁时,使用原子的CAS操作将锁记录中的Mark Word 替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁会膨胀成重量级索。
5、锁的对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗 执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在所竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间 同步块执行速度非常快 |
重量级索 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |