synchronized

众所周知synchronized关键字是解决并发问题常用解决方案,有以下三种使用方式:

  • 同步普通方法,锁的是当前对象。
  • 同步静态方法,锁的是当前Class对象。
  • 同步块,锁的是()中的对象。

实现原理

JVM是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

https://www.wailian.work/images/2018/10/23/synchronized-min.pngsynchronized-min

锁优化

synchronized很多都称之为重量锁,JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

轻量锁

当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中Mark Word拷贝到锁记录中,再尝试使用CASMark Word更新为指向锁记录的指针。

  • 如果更新成功,当前线程就获得了锁。
  • 如果更新失败JVM会先检查锁对象的Mark Word是否指向当前线程的锁记录。
    • 如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
    • 不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁

解锁

轻量锁的解锁过程也是利用CAS来实现的,会尝试锁记录替换回锁对象的Mark Word。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)

轻量锁能提升性能的原因:

  • 认为大多数锁在整个同步周期都不存在竞争,所以使用CAS比使用互斥开销更少。
  • 但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有CAS的开销,甚至比重量锁更慢。

https://www.wailian.work/images/2018/10/23/-min62638.png轻量级锁-min

偏向锁

为了进一步的降低获取锁的代价,JDK1.6之后还引入了偏向锁。

偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。

当线程访问同步块时,会使用CAS将线程ID更新到锁对象的Mark Word中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。

释放锁

当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的Mark Word设置为无锁或者是轻量锁状态。

偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用-XX:-userBiasedLocking=false来关闭偏向锁,并默认进入轻量锁。

https://www.wailian.work/images/2018/10/23/-min.png偏向锁的撤销-min

适应性自旋

在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6加入了适应性自旋:

如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

示例

  • SynchronizedTestSynchronizedProducerConsumer

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。

总结

synchronized现在已经不像以前那么重了,拿1.8中的ConcurrentHashMap就可以看出,里面大量的使用了synchronized来进行同步。