Java锁-Synchronized深层剖析

前言

Java锁的问题,可以说是每个JavaCoder绕不开的一道坎。如果只是粗浅地了解Synchronized等锁的简单应用,那么就没什么谈的了,也不建议继续阅读下去。如果希望非常详细地了解非常底层的信息,如monitor源码剖析,SpinLock,TicketLock,CLHLock等自旋锁的实现,也不建议看下去,因为本文也没有说得那么深入。本文只是按照synchronized这条主线,探讨一下Java的锁实现,如对象头部,markdown,monitor的主要组成,以及不同锁之间的转换。至于常用的ReentrantLock,ReadWriteLock等,我将在之后专门写一篇AQS主线的Java锁分析。

不是我不想解释得更为详细,更为底层,而是因为两个方面。一方面正常开发中真的用不到那么深入的原理。另一方面,而是那些非常深入的资料,比较难以收集,整理。当然啦,等到我的Java积累更加深厚了,也许可以试试。囧

由于Java锁的内容比较杂,划分的维度也是十分多样,所以很是纠结文章的结构。经过一番考虑,还是采用类似正常学习,推演的一种逻辑来写(涉及到一些复杂的新概念时,再详细描述)。希望大家喜欢。

Java锁的相关概念

如果让我谈一下对程序中锁的最原始认识,那我就得说说PV操作(详见我在系统架构师中系统内部原理的笔记)了。通过PV操作可以实现同步效果,以及互斥锁等。

如果让我谈一下对Java程序中最常见的锁的认识,那无疑就是Synchronized了。

Java锁的定义

那么Java锁是什么?网上许多博客都谈到了偏向锁,自旋锁等定义,唯独就是没人去谈Java锁的定义。我也不能很好定义它,因为Java锁随着近些年的不断扩展,其概念早就比原来膨胀了许多。硬要我说,Java锁就是在多线程情况下,通过特定机制(如CAS),特定对象(如Monitor),配合LockRecord等,实现线程间资源独占,流程同步等效果。

当然这个定义并不完美,但也算差不多说出了我目前对锁的认识(貌似这不叫定义,不要计较)。

Java锁的分类标准

  1. 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环(之前文章提到的CAS就是自旋锁)
  2. 乐观锁:假定没有冲突,再修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改(之前文章提到的CAS就是乐观锁)
  3. 悲观所:假定一定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁(Synchronized就是悲观锁)
  4. 独享锁:给资源加上独享锁,该资源同一时刻只能被一个线程持有(如JUC中的写锁)
  5. 共享锁:给资源加上共享锁,该资源可同时被多个线程持有(如JUC中的读锁)
  6. 可重入锁:线程拿到某资源的锁后,可自由进入同一把锁同步的其他代码(即获得锁的线程,可多次进入持有的锁的代码中,如Synchronized就是可重入锁)
  7. 不可重入锁:线程拿到某资源的锁后,不可进入同一把锁同步的其他代码
  8. 公平锁:争夺锁的顺序,获得锁的顺序是按照先来后到的(如ReentrantLock(true))
  9. 非公平所:争夺锁的顺序,获得锁的顺序并非按照先来后到的(如Synchronized)

其实这里面有很多有意思的东西,如自旋锁的特性,大家都可以根据CAS的实现了解到了。Java的自选锁在JDK4的时候就引入了(但当时需要手动开启),并在JDK1.6变为默认开启,更重要的是,在JDK1.6中Java引入了自适应自旋锁(简单说就是自旋锁的自旋次数不再固定)。又比如自旋锁一般都是乐观锁,独享锁是悲观所的子集等等。

** Java锁还可以按照底层实现分为两种。一种是由JVM提供支持的Synchronized锁,另一种是JDK提供的以AQS为实现基础的JUC工具,如ReentrantLock,ReadWriteLock,以及CountDownLatch,Semaphore,CyclicBarrier等。**

Java锁-Synchronized

Synchronized应该是大家最早接触到的Java锁,也是大家一开始用得最多的锁。毕竟它功能多样,能力又强,又能满足常规开发的需求。

有了上面的概念铺垫,就很好定义Synchronized了。Synchronized是悲观锁,独享锁,可重入锁

当然Synchronized有多种使用方式,如同步代码块(类锁),同步代码块(对象锁),同步非静态方法,同步静态方法四种。后面有机会,我会挂上我笔记的相关页面。但是总结一下,其实很简单,注意区分锁的持有者与锁的目标就可以了。static就是针对类(即所有对该类的实例对象)。

其次,Synchronized不仅实现同步,并且JMM中规定,Synchronized要保证可见性(详细参照笔记中对volatile可见性的剖析)。

然后Synchronized有锁优化:锁消除,锁粗化(JDK做了锁粗化的优化,但可以通过代码层面优化,可提高代码的可读性与优雅性)

另外,Synchronized确实很方便,很简单,但是也希望大家不要滥用,看起来很糟糕,而且也让后来者很难下叉。

Java锁的实现原理

终于到了重头戏,也到了最消耗脑力的部分了。这里要说明一点,这里提及的只是常见的锁的原理,并不是所有锁原理展示(如Synchronized展示的是对象锁,而不是类锁,网上也基本没有博客详细写类锁的实现原理,但不代表没有)。如Synchronized方法是通过ACC_SYNCHRONIZED进行隐式同步的。

对象在内存中的结构(重点)

首先,我们需要正常对象在内存中的结构,才可以继续深入研究。

JVM运行时数据区分为线程共享部分(MetaSpace,堆),线程私有部分(程序计数器,虚拟机栈,本地方法栈)。这部分不清楚的,自行百度或查看我之前有关JVM的笔记。那么堆空间存放的就是数组与类对象。而MetaSpace(原方法区/持久代)主要用于存储类的信息,方法数据,方法代码等。

我知道,没有图,你们是不会看的。

PS:为了偷懒,我放的都是网络图片,如果挂了。嗯,你们就自己百度吧

PS2:如果使用的网络图片存在侵权问题,请联系我,抱歉。

第一张图,简单地表述了在JVM中堆,栈,方法区三者之间的关系
在这里插入图片描述

图中展现了对象在无锁,偏向锁,轻量级锁,重量级锁,GC标记五种状态下的Mark Word的不同。

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

引用一下这位大佬的解释哈(毕竟大佬解释得蛮全面的,我就不手打了,只做补充)。

  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

可能你看到这里,会对上面的解释产生一定的疑惑,什么是栈中锁记录,什么是Monitor。别急,接下来的Synchronized锁的实现就会应用到这些东西。

Java锁的内存实现

现在就让我们来看看我们平时使用的Java锁在JVM中到底是怎样的情况。

Synchronized锁一共有四种状态:无锁,偏向锁,轻量级锁,重量级锁。其中偏向锁与轻量级锁是由Java6提出,以优化Synchroniz