首先我们要明确的是加锁的目的:加锁是为了序列化(也就是按顺序)访问临界资源(临界资源就是一个或多个线程想要竞争并修改的对象),即同一时刻只能有一个线程去访问临界资源(此过程称为同步互斥访问)
加锁方式:
1.同步实例方法,锁是当前实例对象2.同步类方法,锁是当前类对象3.同步代码块,锁是括号里面的对象
底层原理:
synchronized属于JVM内置锁,也是隐式锁(隐式锁即不需要手动去加锁和解锁,JVM会自动替我们进行加锁和解锁),当然也有显式锁(ReentranLock)这个后续文章会写到,回到synchronized的底层原理,synchronized是通过内部对象Monitor(监视器锁)实现的,(这里注意下,每个对象都有与之对应的Monitor)那么是如何使用的呢?
synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令,分别在同步块逻辑代码的与结束位置。
监视器锁的底层实现依赖底层操作系统的fMutex lock(互斥锁)实现,它是一个重量级锁,所以性能比较低。
(参考图 1 )
图1
锁升级的大致流程:无锁->偏向锁->轻量级锁->重量级锁(锁升级的过程不可逆)
那么问题来了,我们都知道synchronized加锁是加在对象上的,那对象是如何记录锁的状态呢?
答:锁状态是被记录在每个对象的对象头(Mark Word)中。
*对象的内存布局:
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头,实例数据,对齐填充。
1.对象头(Header): 由Mark Word 和 元数据指针,数组长度 组成。
Mark Word: 由hash码,对象所属的年代(GC的时候会用到),对象锁,锁状态标志,偏向锁ID,偏向时间,数组长度等
2.实例数据(Instance Data) : 即创建对象时对象中成员变量,方法等
3.对齐填充(Padding): 对象的大小必须是8字节的整数倍!

对象头:
HotSpot虚拟机的对象头包括两部分信息。第一部分是"Mark Word",用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向锁ID,偏向时间戳等等。(
这部分数据的长度在32位和 64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了 32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固 定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用 自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于 存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻 量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示)

但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过 Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数 组的大小,所以用一块来记录数组长度。
我们先来分别了解下偏向锁,轻量级锁,自旋锁,锁消除这些概念,因为这在后面讲解锁升级的具体过程中都会有涉及的。
一. 偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过 研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多 次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引 入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模 式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需 再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从 而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效 果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激 烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相 同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏 向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接 着了解轻量级锁.
二. 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种 称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同 步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应 的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就 会导致轻量级锁膨胀为重量级锁
三.自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进 行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都 不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实 现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对 比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程 可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为 自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环 后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作 系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
四.锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编 译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编 译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种 方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的 append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量, 并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情 景,JVM会自动将其锁消除。

锁升级的具体过程:
当只有线程1访问同步块的时候,会去检查对象Object的对象头中的标志位01和 是否偏向01,(当线程1访问之前,Object是出于无锁的状态,所以Object是无偏向的) 那么线程1通过CAS修改Mark Work获取偏向锁,此时由无锁升级为偏向锁。此时Objected的Mark Word 中的线程ID已经指向线程1了

2.这时线程2也来了,也想要访问同步代码块,那么线程2就会去检查Object的Mark Word ,看偏向ID是否是自己,发下并不是指向自己的,那么就会CAS去修改Mark Word 去尝试修改线程ID为自己,(同时我们需要注意的是偏向锁是不会自动释放的) ,由于线程1一直持有者锁在执行,所以线程2就尝试修改失败,(注意此时已经两个线程了),那么线程2就会去找JVM去撤销线程1的偏向锁.
3.同时,如果线程1到达安全点后刚好完成任务(当线程执行到达安全点后才能被中断,不可以随意中断),那么线程1就会释放锁,将TheadID置空,同时偏向由1改为0,线程2通过CAS拿到Object. 如果 没有执行完,就会将锁升级为轻量级锁。
4.同时,线程1线程2都会在各自的线程栈中创建Lock Record区域,复制对象头到Lock Record区域,并在此区域存入owner指针,此时升级成轻量级锁的对象中的前30位存的是指向Lock Record 的指针。线程1和线程2就开始竞争锁,都用CAS去修改Mark Word的前三十位。
如果成功修改到Mark Work前三十位 指向自己线程1的Lock Record,那么线程2就失败了,又开始自旋。线程1执行完之后释放锁,线程2就可以去修改Mark Work前三十位 指向自己线程2的Lock Record。
关于轻量级锁的加锁的简洁版:锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:线程在自己的栈桢中创建锁记录 LockRecord。将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。将锁记录中的 Owner 指针指向锁对象。将锁对象的对象头的 MarkWord替换为指向锁记录的指针


5.如果线程2自旋一定次数后线程1还没有释放,(以Liunux为例) 线程2会让虚拟机去调用PThead 用户态切换为内核态,去把原本指向轻量级锁的指针修改为指向重量级锁Monitor的指针,同时把线程2阻塞挂起。当线程1执行完之后释放锁的时候,发现前三十位已经指向重量级锁了,那么在释放轻量级锁并唤醒被阻塞的线程,进行新一轮的锁竞争。
monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能



喜欢的同学欢迎关注转发!





