深入理解synchronized实现原理

四种锁状态

  1. 无锁状态
  2. 偏向锁
  3. 轻量锁
  4. 重量锁

JVM是如何来识别锁状态的

synchronized关键字是一个对象锁,锁的状态以及谁持有锁,都是记录在这个对象中的,首先我们来看一下一个对象在内存中是如何组成的。

java对象内存组成
从上图可以看出,一个对象的对象头中除了指向该对象的Class对象的指针和数组长度(如果对象为数组,会记录数组的长度)外,还有一个Mark Word。这个Mark Word中主要存储的是对象的运行时数据。如对象的hash值,gc分代年龄,是否为偏向锁,锁的持有线程等信息,如下图所示。

Mark Word
可以看出一个对象的Mark Word的结构会随着锁状态的改变而改变。JVM就是通过锁对象的Mark Word中的信息来识别锁的状态的。

偏向锁实现原理

偏向锁的实现原理非常简单,就是当一个线程来获取锁资源的时候,通过CAS替换将锁对象的Mark Word中的线程ID替换为当前线程,如果替换成功,那么就算获取到锁,替换过程非常快,所以偏向锁的效率非常高。偏向锁值得注意的是,持锁线程即使运行完同步代码块也不会释放锁,只有当其他线程CAS替换失败后,才会通知原持锁线程进行撤销动作。

轻量锁实现原理

进行锁资源争夺的线程会拷贝一份锁对象的Mark Word到自己的线程中(这个拷贝称为锁记录),并且通过CAS替换锁对象中的指针,将其指向线程中的这个锁记录。由于是通过CAS进行替换的,所以只会有一个线程成功,成功的就是那个获取到锁资源的线程,而失败的线程不会立即阻塞,而是会自旋继续尝试获取锁资源(自选的次数是由JVM决定的)。

重量锁实现原理

重量锁的实现原理就是通过一个monitor对象来决定哪个线程获取到了锁,monitor是基于操作系统底层的mutex互斥原语来实现的,由于这个是基础操作系统的,所以就会出现大量的用户态和内核态的切换,这个过程是非常消耗性能的。 锁对象会使用一个指针指向这个monitor对象。

三种锁的优劣比对

锁类型 优点 缺点 适用场景
偏向锁 直接通过CAS替换即可获取锁,效率非常快 由于偏向锁不会主动释放,所以当其他线程来获取锁的时候,会进行额外的撤销动作 比较适用于单线程访问同步代码的情况
轻量锁 通过CAS自旋来尝试获取锁,不会立即将未获取到锁的线程阻塞 由于未获取到锁的线程会进行自旋,会额外的消耗cpu资源 适用于同步代码执行速度较快的场景
重量锁 一旦获取锁失败,就立即阻塞,不会额外消耗CPU资源 这种方式会出现用户态和内核态的切换,效率比较低 适用于同步代码执行速度较慢的场景

锁膨胀过程

下面我们通过一个线程来获取锁资源的过程来讨论一下锁是如何膨胀的。

  1. 当一个线程来获取所资源的时候,首先会判断该锁是否为偏向锁,如果是,则进入2。
  2. 检查Mark Word记录的线程id是否为当前线程,如果是,则说明已经获取到了锁资源,直接运行同步代码,否则进入3。
  3. 通过CAS替换Mark Word中的线程id,如果替换成功,则获取到锁资源,否则进入4。
  4. 等待原持锁线程进入到安全点,并挂起该线程,判断该线程是否退出同步代码
  5. 如果原持锁线程已经运行完毕,则原持锁线程释放锁资源,再次进入到2。
  6. 如果原持锁线程未运行完毕,将锁升级为轻量锁,原持锁线程复制一份锁记录到线程中,并将锁对象的指针指向该锁记录,继续运行同步代码。
  7. 当前线程也会复制一份所记录到自己线程中,并通过CAS自旋来不停的尝试获取锁资源,如果未能在指定的次数内获取到锁资源,进入8。
  8. 将锁升级为重量锁,并挂起当前线程,等待持锁线程释放锁资源后,再次来争夺锁资源。

下面通过一幅流程图来详细的描述整个过程。

锁膨胀过程

锁的一些其他优化

自旋锁和自适应性自旋锁
自旋:当有个线程A去请求某个锁的时候,这个锁正在被其它线程占用,但是线程A并不会马上进入阻塞状态,而是循环请求锁(自旋)。这样做的目的是因为很多时候持有锁的线程会很快释放锁的,线程A可以尝试一直请求锁,没必要被挂起放弃CPU时间片,因为线程被挂起然后到唤醒这个过程开销很大,当然如果线程A自旋指定的时间还没有获得锁,仍然会被挂起。

自适应性自旋:自适应性自旋是自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。例如线程如果自旋成功了,那么下次自旋的次数会增多,因为JVM认为既然上次成功了,那么这次自旋也很有可能成功,那么它会允许自旋的次数更多。反之,如果对于某个锁,自旋很少成功,那么在以后获取这个锁的时候,自旋的次数会变少甚至忽略,避免浪费处理器资源。有了自适应性自旋,随着程序运行和性能监控信息的不断完善,JVM对程序锁的状况预测就会变得越来越准确,JVM也就变得越来越聪明。

锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁粗化
在使用锁的时候,需要让同步块的作用范围尽可能小,这样做的目的是为了使需要同步的操作数量尽可能小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

参考:深入分析synchronized原理和锁膨胀过程

发布了81 篇原创文章 · 获赞 16 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/mazhen1991/article/details/90409602