synchronized 关键字原理
众所周知 synchronized
关键字是解决并发问题常用解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前
Class
对象。 - 同步块,锁的是
()
中的对象。
实现原理:JVM
是通过进入、退出对象监视器( Monitor
)来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter
指令,在退出方法和异常处插入 monitor.exit
的指令。
其本质就是对一个对象监视器( Monitor
)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit
之后才能尝试继续获取锁。
流程图如下:
通过一段代码来演示:
1 | public static void main(String[] args) { |
使用 javap -c Synchronize
可以查看编译之后的具体信息。
1 | public class com.crossoverjie.synchronize.Synchronize { |
可以看到在同步块的入口和出口分别有 monitorenter,monitorexit
指令。
锁优化
synchronized
很多都称之为重量锁,JDK1.6
中对 synchronized
进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁
和轻量锁
。
轻量锁
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record
)区域,同时将锁对象的对象头中 Mark Word
拷贝到锁记录中,再尝试使用 CAS
将 Mark Word
更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM
会先检查锁对象的 Mark Word
是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
解锁
轻量锁的解锁过程也是利用 CAS
来实现的,会尝试锁记录替换回锁对象的 Mark Word
。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁
)
轻量锁能提升性能的原因:
认为大多数锁在整个同步周期都不存在竞争,所以使用 CAS
比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS
的开销,甚至比重量锁更慢。
偏向锁
为了进一步的降低获取锁的代价,JDK1.6
之后还引入了偏向锁。
偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。
当线程访问同步块时,会使用 CAS
将线程 ID 更新到锁对象的 Mark Word
中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
释放锁
当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 Mark Word
设置为无锁或者是轻量锁状态。
偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 -XX:-userBiasedLocking=false
来关闭偏向锁,并默认进入轻量锁。
其他优化
适应性自旋
在使用 CAS
时,如果操作失败,CAS
会自旋再次尝试。由于自旋是需要消耗 CPU
资源的,所以如果长期自旋就白白浪费了 CPU
。JDK1.6
加入了适应性自旋:
如果某个锁自旋很少成功获得,那么下一次就会减少自旋。
总结
synchronized 现在已经不像以前那么重了,拿 1.8 中的 ConcurrentHashMap 就可以看出,里面大量的使用了 synchronized 来进行同步。
号外
最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。