一、Java的锁
1. 悲观锁和乐观锁
首先我们需要明白一个概念,资源或者变量如果只是并发读是不会产生冲突的,我们所说的并发冲突都是并发写,或者既有读又有写。 悲观锁是指当对一个资源进行加锁时,默认这个资源是并发不安全的,即默认所有并发操作都会导致冲突,可以理解为悲观锁认为所有对资源或变量的并发操作都是写操作。一旦有一个线程拿到资源,其他线程都会被直接阻塞等待,synchronized和ReentrantLock都是悲观锁的具体实现, 乐观锁正好相反,默认所有资源都是并发安全的,乐观锁在获取资源时不会阻塞其他线程,一旦更新资源时发生冲突则会触发错误并返回。举个例子,假设有一个多线程共享的变量a = 10,此时threadA获取a并执行 a= a+10; 而在threadA没有更新完之前,threadB也获取了a,并执行a= a +20,则此时会出现冲突并返回。悲观锁一个典型的实现就是CAS(compare and swap), CAS在更新变量时会同时记录两个值,oldValue 和 newValue,分别是更新前的值和新的值,对于刚才的例子,如果采用了CAS机制,则threadB在更新a的值时,会同时携带oldValue = 10和newValue=30,此时由于threadA已经将a的值更新为20,会导致threadB获取到的当前值和oldValue不一致,此时threadB会返回并重新获取a的值,重新执行,重复此过程直到不冲突为止。
2. 公平锁和非公平锁
公平锁是指多个线程在竞争锁时,按照时间优先级排队,简单来说就是先来后到,先来排队的线程也会优先获取到锁。 非公平锁则是在竞争锁时并不会考虑竞争时间,也就是先来的可能会竞争失败继续排队。synchronized就是一种典型的非公平锁。
3. 锁的自旋
4. 偏向锁、轻量级锁、重量级锁
偏向锁: 当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取/竞争锁带来的消耗,即提高性能。 引入偏向锁的主要目的是:在无多线程竞争的情况下尽量减少不必须要的轻量级锁执行路径。其实在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁就可以减少很多不必要的性能开销和上下文切换。Java中的可重入锁ReentransLock就是一种典型的偏向锁。
轻量级锁: 轻量锁认为环境中不存在竞争或者竞争不激烈,所以轻量级锁一般都只会有少数几个线程竞争锁对象,其他线程只需要稍微等待(一般是通过自旋实现等待,自旋的原理在后面说)下就可以获取锁,但是自旋次数有限制(默认一般是10次,可自行配置), 引入轻量级锁的主要目的是:在多线程竞争不激烈的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。
重量级锁: 我们知道synchronized在对资源/代码块加锁时,只有一个线程可以获取资源,其他线程都会被阻塞,其底层是基于CPU的互斥量实现,在一些线程竞争较为激烈的场景,会出现线程被频繁阻塞——>竞争——>阻塞,最终可能导致线程的执行时间远低于等待时间。本质是因为竞争——>阻塞过程中线程需要在挂起和执行间切换,这个过程是操作系统完成的,操作系统在底层上会通过切换用户态和内核态实现这一功能,但这个操作非常消耗CPU资源。这种我们称之为重量级锁。
二、锁的膨胀
1. 锁的底层构造
前面介绍了Java中锁的分类,需要说明的是上面说的锁的分类是基于不同维度的,所以一种锁的具体实现往往可以命中多个分类,比如synchronizd是一种悲观、非公平重量级锁。 锁的底层构造其实就是说Java在底层是如何标识一个对象是否加锁、加了什么类型锁。
1.1. 对象头
以HotSpot虚拟机为例,每一个对象在堆内存上存储时,除了我们知道的会存储对象的内部的属性值外,其实还会存储一个对象头,对象头包括两部分内容:Mark Word(标识字段)和Class Pointer(类型指针),类型指针主要是存储此对象对应的类信息(不是类的完整信息,类的完整信息在方法区,这里的指针只是指向方法区上的对应类)。 Mark Word默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。
1.2 从锁的构造理解偏向锁
当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
1.3 轻量级锁膨胀为重量级锁的过程
前面说到,轻量级锁在竞争时,竞争失败的不会直接挂起阻塞,而是会进行自旋等待,但这个自旋显然不能无限持续下去,因为自旋本身也会耗费CPU资源。如果自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
|