Java 中各种锁的概念
@[toc]
---
## 悲观锁 | 乐观锁
悲观锁和乐观锁不仅在 Java 中,各位学习数据库的时候应该也都遇到过。
1)**悲观锁(Pessimistic Lock)的定义**:悲观锁是一种悲观思想,认为自己在使用数据的时候一定有别的线程来修改数据,因此<u>在获取数据的时候会先加锁,确保数据不会被别的线程修改</u>。
`synchronized` 关键字和 `Lock` 接口的实现类就是悲观锁。
2)**乐观锁(Optimistic Lock)的定义**:乐观锁是一种乐观思想,<u>认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在提交修改的时候去判断一下,在此之前有没有其他的线程也修改了这个数据</u>:
- 如果其他的线程还没有提交修改,那么当前线程就将自己修改的数据成功写入;
- 如果其他的线程已经提交了修改,则当前线程会根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在 Java 中是采用 CAS 算法实现的,J.U.C 包中的原子类 `Atomic` 就是通过 CAS 算法来实现了乐观锁。
使用这种 CAS 算法的代码也常被称为**无锁**编程(Lock-Free)。
> 简单介绍下 CAS,下面会详细解释的。CAS(Compare And Set),比较并替换,比较当前值(主内存中的值)与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,如果一样则执行更新,如果不一样则不执行更新。
>
> 
>
> 很多文章会把【CAS】和【CAS 自旋】等同起来,事实上,CAS 并不总是伴随着自旋操作的(CAS 自旋就是,如果当前值与预期值不一样,则通过 while 循环进行重试)!!!比如轻量级锁竞争过程中的 CAS 就并不伴随着自旋操作!(下文会解释)
>
> 并且,也不要把 CAS 和乐观锁等同起来,我觉得这样非常不利于小白的理解,CAS 它是一种算法,用这种算法可以实现乐观锁,也可以实现其他东西。就好比数组,它是一种工具,可以用来实现链表,也可以用来实现线性表。
**悲观锁和乐观锁的使用场景**:https://blog.csdn.net/ahjxhy2010/article/details/80519664
- 乐观锁适用于【写操作比较少】的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果写操作比较多经常产生冲突,上层应用就会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适
## 自旋锁 | 自旋适应锁
前文说过,Java 中的线程是与操作系统中的线程一一对应的,所以**阻塞或者唤醒一个 Java 线程是需要操作系统切换到内核态来完成的**,这显然给程序的并发能力带来了一定的性能损耗。
正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就显得有点多余了。甚至,如果同步代码块中的内容比较简单,同步资源被锁的时间比较短,可能操作系统状态转换消耗的时间比用户代码执行的时间还要长。
所以说,为了这一小段时间去切换操作系统状态是得不偿失的。
现在绝大多数的计算机都是多核处理器系统,支持两个或以上的线程同时并行执行,综合上述考虑,我们可以让后面请求锁的那个线程 “稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。
这个 “稍等一下” 就是自旋,**从代码的角度来说,自旋其实就是通过 `while` 循环进行重试**。
如果这个线程自旋完成后,前面锁定共享资源的线程已经释放了锁,那么这个线程就可以不必被阻塞而是直接获取共享资源,从而避免切换操作系统状态的开销。
这就是**自旋锁**。
> 拿段源码出来看看,比如 AtomicInteger 中的自增方法 getAndIncrement:
>
> 
>
> 所以说,Java 中自旋锁的使用就体现在 CAS 比较操作失败后的自旋等待(CAS 自旋),这也就是为什么说<u>自旋锁其实是基于 CAS 的一种锁实现</u>
如果这个 while 循环里面的条件一直无法满足呢?也就是说 CAS 比较操作一直失败呢?难道就这样一直无休止地自旋等待下去吗?
答案是:不会一直自旋等待下去。
因为**自旋等待虽然避免了线程切换的开销,但它既然在循环就肯定需要占用处理器时间。那如果锁被占用的时间很短,自旋等待的效果就会非常好。但是,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源,空转,死等,还不如被阻塞住**。这也就是为什么我们说:纵使阻塞会使操作系统陷入内核态,但是它仍然无法被自旋锁替代。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数没有成功获得锁,就应当挂起线程。
默认是 10 次,可以使用参数 `-XX:PreBlockSpin` 来更改。
在 JDK 1.6 中,对于自选等待的次数这个问题,做出了一次优化,即引入了**适应性自旋锁**(自适应的自旋锁)。
自适应意味着自旋的次数(时间)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么 JVM 就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间;
- 如果对于某个锁,很少有线程通过自旋等待成功获得过,那么当以后有线程尝试获取这个锁时, JVM 可能省略掉自旋过程,直接阻塞住线程,避免空转浪费处理器资源。
## 公平锁 | 非公平锁
**公平锁**:「多个线程按照申请锁的顺序来获取锁」。在并发环境中,每个线程会先查看此锁维护的等待队列,如果该锁的等待队列为空,则直接占有锁;如果该锁的等待队列不为空,则该线程加入到等待队列的末尾,按照 FIFO 的原则从队列中取出线程,然后占有锁。
**非公平锁**:线程会先尝试获取锁,如果获取不到,则再采用公平锁的方式也就是进入等待队列。也就是说,「多个线程获取锁的顺序,不是按照 FIFO 的顺序,有可能后申请锁的线程比先申请的线程优先获取到锁」。
**通俗来讲就是,获取公平锁需要排队,而获取非公平锁优先选择插队,如果插队失败,再采取排队的方式**。
公平锁和非公平锁优缺参半。
- 公平锁的优点就是等待锁的线程不会饿死
缺点就是整体效率比非公平锁要低,因为等待队列中除了队头的第一个线程以外,其他所有线程都会被阻塞住,而阻塞线程的唤醒需要操作系统陷入内核态。
公平锁适用于线程竞争比较激烈且每个线程等待的时间差别不大的情况,因为公平锁能够保证锁的获取是按照请求的顺序进行的,不会出现某个线程一直获取不到锁的情况,避免了"饥饿"现象。在一些对锁获取顺序要求比较严格的场景中,如数据库的行级锁,公平锁也更为适用。
- 非公平锁的优点是相对于公平锁来说可以减少唤醒线程的开销,整体的性能比较高,因为线程有几率不阻塞直接获得锁。适用于对锁获取顺序不敏感的高并发场景,以便于获得更好的并发度。
缺点就是处于等待队列中的线程可能会饿死,或者说等待很久才能获得锁(饥饿)。
在 Java 中,synchronized 就是非公平锁的实现,Lock 接口的实现类 ReentrantLock 可以通过构造函数来指定该锁是公平的还是非公平的(默认是非公平的)。

## 可重入锁 | 不可重入锁
**可重入锁**:也称为递归锁,同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。<u>简单来说就是一个线程可以重复获取同一个锁</u>
**不可重入锁**:<u>简单来说就是一个线程不可以重复获取同一个锁</u>,如果某个线程获取一个锁后想要再次获取该锁,需要先释放再重新获取
> 很多文章会直接用 ReentrantLock 代替可重入锁这个名词,那对于萌新来说是有一定误导性的。除了 ReentrantLock,synchronized 也是可重入锁,**可重入锁的一个优点是可一定程度避免死锁**。
>
> 以 synchronized 举例:
>
> 
>
> 类中的两个方法都是被 synchronized 修饰的,func1 方法中调用了 func2 方法,因为 synchronized 是可重入锁,所以同一个线程在调用 func1 时可以直接获得当前对象的锁,进入 func2 进行操作。
>
> 而如果是不可重入锁,那这段代码就会出现死锁。
>
> 不可重入锁就是说,当前线程在调用 func2 之前需要将执行 func1 时获取到的对象锁释放掉,然而,由于 func2 是在 func1 内部被调用的,也就是说该对象锁正在被当前线程所持有,并且无法释放。所以造成死锁。
>
> 以 ReentrantLock 为例,我们来分析下可重入和不可重入的底层原理,其实还是刚刚分析公平锁非公平锁那段 Sync 中的代码:
>
> 
>
> 我先来说下上述代码中的 getState 方法 get 到的 state 到底是个啥。
>
> state 就是 ReentranLock 的父类 AQS 维护的一个同步状态 status,用来表示锁的重入次数,初始值为 0。
>
> 
>
> 当线程尝试获取可重入锁时,会先获取 state 值,如果 state = 0 表示没有其他线程正在占用这把锁,则把 state 置为 1,当前线程开始执行;
>
> 如果当前线程获取到的 state != 0,则 JVM 会判断当前线程是否是占用这把锁的线程,如果是的话则执行 state + 1,且当前线程可以再次获取锁。(synchronized 可重入的原理其实和这也差不多)
>
> 而如果是非可重入锁,那可想而知,如果 state != 0 的话就会直接判定获取锁失败,则当前线程被阻塞住。
>
> OK,上面讲的是加锁方法,再来看看如何释放掉可重入锁:
>
> 
>
> 简单来说,就是在当前线程是持有锁的线程的前提下,每执行一次释放锁操作,就将 state - 1,只有当 state = 0 的时候,该线程才会真正地释放锁。
## 共享锁 | 排他锁
**排他锁**:也称互斥锁、独享锁,该锁一次只能被一个线程所持有。如果线程 A 对数据 B 加上排它锁后,则其他线程不能再对 B 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
Java 中的 synchronized 和 ReentrantLock 就是排他锁。
> 为什么说 synchronized 和 ReentrantLock 是排他锁,解释一下。其实就是,如果在当前线程不是拥有锁的线程的时候,就不能添加锁,也就是说锁一次只能被一个线程所持有
排他锁是一种悲观锁,每次访问资源都先加上排他锁,但是读操作其实并不会影响数据的一致性,而排他锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取,这限制了并发性。
**共享锁**:该锁可被多个线程所持有。如果线程 A 对数据 B 加上共享锁后,则其他线程只能对 B 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
共享锁是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
> ReentrantReadWriteLock(读写锁)中的读锁就是共享锁。
>
> 顾名思义,ReentrantReadWriteLock 中包含两种锁:读锁 ReadLock(共享锁)和写锁 WriteLock(排他锁):
>
> 
