Synchronized低效的原因
在Java SE 1.6发布前,使用Synchronized
关键字实现同步功能是比较低效的,很多人称其为重量级锁.究其原理,是因为Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,而监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
Java SE 1.6为Synchronized带来的优化(偏向锁和轻量级锁)
Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁.要想弄清楚这两个锁的原理首先要了解Java对象头,因为Synchronized
用的锁就存在Java对象头中.
Java对象头
Java对象头的主要内容
内容 | 说明 |
---|---|
Mark Work | 存储对象的hashCode或锁信息等 |
Class Metadata Address | 存储到对象类型数据的地址(即指向该对象的类型数据的指针) |
Array Length | 数组的长度(如果当前对象是数组) |
32位的JVM的Mark Work的默认存储结构
锁状态 | 25bit | 4bit | 1bit 是否为偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
Mark Word存储结构中的数据会随着标志位的变化而变化.
Mark Word可能的状态变化
在Java SE 1.6中,锁一共有4中状态,级别从低到高依次为:
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
值得注意的是锁状态只能升级不能降级,也就是说轻量级可以膨胀为重量级锁,但是这个过程不可以逆转.
1.偏向锁
为何引入偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得.
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)
偏向锁是在只有一个线程执行同步块时进一步提高性能。
偏向锁的获取过程:
- 访问对象头的
Mark Word
中的锁标志位是否为01(确认为可偏向状态) - 如果为可偏向状态,则测试对象头中
Mark Word
的线程ID是否指向当前线程,如果是则进入步骤5,否则进入步骤3 - 如果对象头中
Mark Word
的线程ID并未指向当前线程,则当前线程通过CAS操作竞争锁.如果竞争成功,则将Mark Word
中线程ID设置为当前ID,然后执行步骤5.如果竞争失败,则执行步骤4. - 如果CAS竞争偏向锁失败,则表示有竞争.当到到全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码.
- 执行同步代码
注意:第四步中到达安全点safepoint会导致stop the word,时间很短。
偏向锁的撤销
使用一种等到竞争出现才释放锁的机制,即当其他线程竞争偏向锁时,持有偏向锁的线程会运行到全局安全点后挂起,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
2.轻量级锁
为何引入轻量级锁
轻量级锁是为了在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
轻量级锁的加锁过程:
- 在线程进入同步代码块时,如果同步对象锁状态为无锁状态(锁标志位为01,可否可偏向锁为0),JVM会首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的
Mark Word
的拷贝,官方名称为’Displaced Mark Word’.这个时候线程堆栈与对象头的状态如图1所示. - 拷贝对象头的
Mark Word
复制到当前线程栈帧的锁记录中. - 拷贝成功后,虚拟机将使用CAS操作尝试将对象的
Mark Word
更新为指向锁记录(Lock Record)的指针,并将锁记录(Lock Record)里的owner指针指向所对象的Mark Word
.如果CAS更新操作成功,则执行步骤4,否则执行步骤5. - 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象
Mark Word
的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2所示。 - 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁.
轻量级锁的撤销
- 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
- 如果替换成功,整个同步过程就完成了。
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
3.锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比时间相差无几 | 如果线程存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不会自旋,不会消耗CPU | 线程阻塞,响应速度慢 | 追求吞吐量,同步块执行速度慢或者执行时间长. |
Java 并发编程(一)Volatile原理剖析及使用
Java 并发编程(二)Synchronized原理剖析及使用
Java 并发编程(三)Synchronized底层优化(偏向锁与轻量级锁)
Java 并发编程(四)JVM中锁的优化
Java 并发编程(五)原子操作类