synchronized锁升级过程 悲观锁 乐观锁 偏向锁 MarkWord锁标识位

81 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

synchronized基础概念

  • 要完成原子性操作,只能靠锁来实现
  • 上锁的本质就是把并发操作转变为序列化操作
  • 修饰方法锁的是对象,修饰静态方法锁的是类的class,class只有一个
  • 抛出异常,锁会被释放
  • synchronized锁对象时不要用Integer、String、Long类型
  • Integer和Long会有自动拆箱和装箱的过程,改变值后,可能不会是原来的对象,同步块就会失效
  • String因为有常量池的存在,值一致时,指向的地址会一致,会出现两个线程要锁的是同一个对象
  • JDK早期,synchronized上的是重量级锁
  • 目前的synchronized包含重量级锁,轻量级锁和偏向锁
  • synchronized可以保证可见性和原子性但是不能保证有序性
  • synchronized是可重入锁,重入次数必须得记录,解锁时要对应,记录的位置就在MarkWord里面
  • synchronized内部自带的轻量级锁和偏向锁,都不用向操作系统打交道,只需要在用户态就可以完成
  • 开发中通常选择synchronized,因为目前的synchronized内部做了很多优化,有锁升级的机制,具备自适应功能
  • synchronized优化指的就是把锁的粒度变粗或者变细

悲观锁(重量级锁)

  • 和锁关联的会有一个队列(WaitSet:重量级锁的队列),让后面的线程在队列中排队
  • 线程执行时间长,竞争的线程比较多,用悲观锁,可以不占用CPU资源

乐观锁(轻量级锁,自旋锁,CAS)

  • 乐观锁的原理就是执行时不上锁,执行完后比较这个资源是不是执行前的资源
  • 如果是,则替换结果,如果不是,则会拿当前资源再去执行一遍,形成自旋过程
  • ABA问题:比较资源时,资源A可能之前被改成过B,然后又改回了A,如果资源是引用,则无法知道里面的值是否一样
  • ABA解决办法:可以在资源上添加一个版本号解决
  • 乐观锁的自旋过程是会消耗CPU资源的
  • 线程执行时间短,竞争的线程比较少,再用乐观锁
  • Java中用AtomicIntege可以实现乐观锁
  • 乐观锁中的比较和交换两个指令必须要是原子操作
  • 所以追源码可以发现AtomicIntege底层最终实现的汇编指令是lock cmpxchg

偏向锁

  • 偏向锁就是只有一个线程来的时候,由synchronized把这个线程的ID(c++中叫线程指针)写到MarkWord里面
  • 单线程的时候,偏向锁没有竞争,所以效率很高
  • 当产生竞争时,偏向锁会撤销
  • 偏向锁的启动会有一个4秒延迟
  • 因为JVM启动时有很多默认线程,里面很多synchronized代码,如果一来就启动偏向锁,会涉及到大量的锁撤销和锁升级操作,效率会很低

synchronized锁升级过程

  • 普通对象的情况下,单线程访问,synchronized会使用偏向锁,多线程访问,synchronized会直接使用轻量级锁
  • 当对象属于匿名偏向态时,单线程和多线程访问,产生的锁都会为偏向锁
  • JDK11中,默认都是匿名偏向态(依然存在4秒延迟),标记为101,JDK8中,默认都是无锁 的普通对象,标记为001
  • 当产生竞争时,偏向锁会撤销,线程之间开始竞争,每个线程内部都会生成一个LR(Lock Record 锁记录),此时锁升级为轻量级锁
  • 当竞争加剧时,会向操作系统申请资源,linux mutex
  • 线程挂起,进入等待队列,等待操作系统调度,然后再映射回用户空间,至此升级为重量级锁
  • 竞争加剧:有线程超过10次自旋或者自旋线程数超过CPU核数一半,1.6之后JVM会自己控制
  • synchronized锁的升级过程就是通过修改Markword的最低两位来做的升级,倒数第三位是偏向锁的标记位

在这里插入图片描述