synchronized

synchronized 和 lock 锁底层的实现原理?

第七章 ReentrantLock 总结_chuifuhuo6864 的博客-CSDN 博客
synrhronized 关键字简洁、清晰、语义明确,因此即使有了 Lock 接口,使用的还是非常广泛。
其应用层的语义是可以把任何一个非 null 对象作为”锁”。
在 HotSpot JVM 实现中,锁有个专门的名字:对象监视器。
synchronized 是 jvm 层面的实现,lock 是 jdk 层面的实现。

synchronized 加锁对象:

  1. 当 synchronized 作用在方法上时,锁住的便是对象实例(this)。
  2. 当作用在静态方法时锁住的便是对象对应的 Class 实例,因为 Class 数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;
  3. 当 synchronized 作用于某一个对象实例时,锁住的便是对应的代码块。
  4. 修饰实例方法:对实例的对象进行加锁
  5. 修饰类的方法:对类对象进行加锁
  6. 修饰代码块:锁括号里面的对象进行加锁

synchronized 底层实现原理:

  1. synchronized 通过 monitor 和 mutex lock 实现了互斥
  2. synchronized 是通过对象内部的一个叫做监视器(monitor)来实现的
  3. 监视器内有个组件是计数器,默认值为 0
  4. 当计数器为 0,尝试加锁,加锁成功,计数器加 1
  5. 其他线程发现计数器不为 0,加锁失败进入阻塞

synchronized 也实现了可重入的功能,
监视器锁本质是依赖于底层的操作系统的 Mutex Lock 是互斥锁来实现的。
Mutex Lock 指令的调用需要从用户态转换到核心态,
成本非常高,造成 synchronized 效率低,
因此 synchronized 称其为“重量级锁”,
JDK 对 synchronized 进行了优化,引入了膨胀的过程:

无锁->偏向锁->轻量级锁->重量级锁即 Mutex Lock

什么是 ReetrantLock?

Java 中 Lock 接口提供了 ReetrantLock 实现类来实现可重入锁功能。
ReetrantLock 基于 AQS (抽象队列同步器)来实现加锁的获取、可重入、释放。

实现可重入的功能: 可以重复地获取自己所拥有的锁

ReetrantLock 实现原理:

锁内部有两个核心参数:
state:计数器
exclusiveOwnerThread:所有者线程

  1. 先进行 cas 算法,是原子性的同时只能一个线程,当 state 计数值为 0 时,这个锁就认为是没有被任何线程所占有的,其他线程会进入等待队列。
  2. 当一个线程请求一个未被持有的锁时,计数值将会递增。
  3. 而当线程退出同步代码时,计数值会相应的递减。当计数值为 0 时,则释放该锁。

区别:

synchronizedReetrantLock
公平非公平锁参数 true/false 可以指定公平锁还是非公平锁,公平锁即进入等待队列,先进先出
队列由于等待队列,可提供中断等待锁的线程的机制
粒度粒度较粗,只有对象类代码块三种方法实现粒度较细,调用方法更多
自动释放退出时编译器自动保证释放锁手工主动释放锁,一定记得 finallly 代码块中调用 unlock()释放锁

synchronized 的锁升级过程?

synchronized 用的锁存在 Java 对象头里,Java 对象头里的 Mark Word 默认存储对象的 HashCode、分代年龄和锁标记位。在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。
32 位 JVM 的 Mark Word 可能变化存储为以下 5 种数据:

锁状态25bit4bit1bit2bit
23bit2bit是否偏向锁锁标志位
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC 标志11
偏向锁线程 IDepoch对象分代年龄101
无锁对象 hashcode对象分代年龄001

锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。为了提高获得锁和释放锁的效率,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。

无锁状态->偏向锁->轻量级锁->重量级锁

偏向锁:

当我们创建一个对象时,该对象的部门 Markword 关键数据如下:

bit fields是否偏向锁锁标志位
hash001

该对象被创建出来的那一刻,就有了偏向锁的标志位,这说明所有对象都是可偏向的,但所有对象的状态都为 0,这说明被创建的对象的偏向锁并没有生效。

当线程执行到临界区时,此时会利用 CAS 操作,将线程 ID 插入到 Markword 中,同时修改偏向锁的标志位。
此时 Markword 的关键信息如下:

bit fields是否偏向锁锁标志位
hashepoch101

此时偏向锁状态是 1,说明偏向锁生效了,同时也可以看到,那个线程获得了该对象的锁。

偏向锁是 jdk1.6 引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

也就是说:在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

  1. Load-and-test,判断当前线程 id 是否与 markword 中的线程 id 相同。
  2. 如果一致,说明此线程已经获得了对象的锁,继续执行下面的代码。
  3. 如果不一致,检查对象是否可偏向。
  4. 如果还未偏向,采用 CAS 操作来竞争锁。

释放锁:

偏向锁不会主动释放,采用了一种只有竞争才会释放锁的机制,不会主动释放,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点。
步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态。
  2. 撤销偏向锁,恢复到无锁状态,或者轻量级锁状态。

安全点会导致 stw(stop the word),导致性能下降,这种情况应该禁用。

jvm 开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁:

自旋锁的目标是降低线程切换的成本。如果竞争激烈,我们不得不采用重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁是浪费的。轻量级锁的目标是,减少无实际竞争的情况下,使用重量级锁产生的实际消耗,包括系统调用引起的内核态与用户态的切换、线程阻塞造成的线程切换等。

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时不需要申请互斥量,仅仅将 Mark Word 中的部分字节 CAS 更新指向线程栈中的 Lock Record(Lock Record:JVM 检测到当前对象是无锁状态,则会在当前线程的栈帧中创建一个名为 LOCKRECOD 表空间用于 copy Mark word 中的数据),如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后在膨胀为重量级锁。

** 缺点:**同自旋锁相似:如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。

重量级锁:

轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖操作系统的 MutexLock 来实现,所以重量级锁也被称为互斥锁。
当轻量级锁经过锁撤销等步骤升级为重量级锁后,它的 Markword 部分如下:

bit fields锁标志位
指向 Mutex 的指针10

重量级锁开销大:
当系统检查到锁是重量级锁后,会把等待想要获得锁的线程阻塞,被阻塞的线程不会消耗 CPU。但阻塞或唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的。

互斥锁(重量级锁)也称为阻塞同步、悲观锁。

总结:

偏向锁,轻量级锁是乐观锁,重量级锁是悲观锁。

一个对象刚开始实例化的时候,没有任何线程来访问它。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,他会偏向这个线程,此时对象持有偏向锁。
偏向第一个线程,这个线程在修改对象头成为偏向锁的时候采用 CAS 操作,并将对象头中的 ThreadID 修改为自己的 ID,之后再次访问这个对象时,只需要对比 ID,不需要在使用 CAS 进行操作。

一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象的偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍需要持有偏向锁,则偏向锁升级为轻量级锁。如果不存在使用了,则可以将对象恢复为无锁状态,然后重新偏向。

轻量级锁认为竞争存在,但竞争的程度很轻,一般两个线程对于同一个线程的操作都会错开,或者稍微等待一会,另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程持有锁,一个线程在自旋,又有第三个线程来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转。

区别:

优点缺点场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法仅存在纳秒级的差距如果线程间存在锁竞争,会存在额外的锁撤销的消耗只有一个线程访问同步快的场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗 CPU追求响应时间,同步块执行非常快
重量级锁线程的竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间缓慢追求吞吐量,同步快执行时间较长

synchronized 的锁方法,锁对象头,锁代码块,怎么实现?

锁方法

1
2
3
public synchronized void method() {
// todo
}

锁方法,锁住了整个方法的内容,同时只能有一个线程进入该方法。

1
2
3
4
5
public void method() {
synchronized(this) {
// todo
}
}

锁代码块,锁住了整个方法的内容,同时只能有一个线程进入该方法。

锁代码块

1
2
3
4
5
public void method() {
synchronized (this) {
// todo
}
}

锁代码块,锁住了整个方法的内容,同时只能有一个线程进入该方法,就是上一个。

锁对象

1
2
3
4
5
6
private final Object object = new Object();
public void method() {
synchronized (object) {
// todo
}
}
1
2
3
4
5
public void method(Object object) {
synchronized (object) {
System.out.println(1);
}
}

以一个明确的对象作为锁,那个线程拿到锁就可以执行,其他线程必须等待该线程访问结束。
没有明确对象时,可以创建一个特殊对象作为锁。

锁类

1
2
3
4
5
6
7
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}

锁静态方法

1
2
3
public synchronized static void method() {
// todo
}

锁静态方法,与锁类相同,会锁住这个类的所有对象,即同时只能有一个线程,进入该类,执行程序。
该类的所有对象公用一把锁。

总结

  1. 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象公用同一把锁。
  2. 每个对象只有一个锁与之关联,谁拿到这个锁谁就可以运行他所控制的那段代码。
  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

sy