目录
介绍
实现原理
对象头
Monitor(监视器)
锁升级
偏向锁
轻量级锁
重量级锁
锁的优缺点
介绍
synchronized 是 Java 中的关键字,它用于锁定代码块或方法,以确保同一时刻只有一个线程可以进入被锁定的部分。这在多线程编程中非常重要,因为它有助于避免多个线程同时访问共享资源时引发的数据不一致问题。synchronized 可以锁定对象,也可以锁定类。锁的对象决定了哪些线程可以进入锁定区域。
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现
为以下3种形式。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
实现原理
Synchronized的底层实现是完全依赖JVM虚拟机的,因此synchronized的底层原理一定会涉及到JVM对象存储中的对象头和Monitor(监视器),想要了解底层原理需要从这两方面入手。
对象头
真正标志某个对象是否上synchronized锁的位置就是对象头。
对象的头信息中存放着类信息和锁信息,如果数组的话还会存放着长度
对象头可以分为三部分,锁信息在第一个部分Mark Word。32位操作系统前两部分是32bit,如果是64位操作系统前两部分就是641bit。
锁相当于一个标记,一个俗称标记
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。下图位32位虚拟机对象头中MarkWord中的信息。
如果一个线程对对象头部修改成功加锁了,其他线程就不能再加锁了
Monitor(监视器)
jdk1.6之前,synchronized
只能实现重量级锁,Java虚拟机是基于Monitor对象来实现重量级锁的。
随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程。
什么是Monitor?
每个 Java 对象都关联一个 monitor,也称为监视器或管程,用于实现对象级别的锁定。
当一个线程进入一个 synchronized
代码块或方法时,它会尝试获取该对象的 monitor。如果该 monitor 已被其他线程占用,则当前线程会被阻塞,直到获取到该 monitor。
如何使用?
在JVM中是基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现,同时也可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
锁升级
上面提到了1.6中为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,因此锁就有了一个升级的过程。
首先上了锁之后,会先上偏向锁,偏向锁升级后会变为轻量级锁,轻量级锁升级后会变为重量级锁。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧(栈帧指的就是方法)中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
这里需要注意:
因为头部锁信息占8字节,总线不够用,不能一次全部更改完,一个线程在更新时要改两次,因此加锁时要用CAS进行比较,比较时符合再操作,两次修改时都要比较,以防止第一次改完后别的线程再进行更改,只要是刚往回更新就不允许别的线程修改了。
偏向锁的特性:偏向锁在线程执行完依然不解锁,下次再执行时也无需再上锁
轻量级锁
什么是轻量级锁:
一旦有其他线程跟加偏向锁的线程竞争,偏向锁就会自动升级为轻量级锁,竞争失败的线程会进入就绪队列,并一直死循环进行探测,进行CAS看能否进行替换(这个过程叫做自旋,也就是死循环),一旦上锁的线程执行完,直接给就绪队列中的线程加上锁。(适用于线程竞争小,每个线程的执行时间较短的情况)
轻量级锁加锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁的特点:一旦其他线程释放锁,它能以最快的速度感知到,并加上锁
重量级锁
当有很多进程竞争,并且正在运行的加锁线程需要运行很长时间时,失败后进行自旋时,会造成性能的浪费,此时升级为重量级锁,让失败的线程在进行一次CAS比较失败后就进入阻塞队列,以减少性能的浪费。
锁的升级过程不可逆,一旦成了重量级锁就不能变为偏向锁和轻量级锁了