乐观锁和悲观锁
锁的实现者要预测接下来锁冲突的概率是大还是小, 根据锁冲突概率, 来决定是用乐观锁还是悲观锁.
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞等待, 直到它拿到锁.
乐观锁
假设线程一般情况下不会产生锁冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生锁冲突进行检测,如果发现锁冲突了,则让返回用户错误的信息,让用户决定如何去做.
总结
悲观锁 : 预测接下来发生锁冲突概率比较大.
乐观锁 : 预测接下来发生锁冲突概率比较小.
通常来说, 悲观锁要做的工作要更多一点, 效率也更低一点. 乐观锁做的工作要更少一点, 效率要更高一点.
重量级锁和轻量级锁
重量级锁 : 涉及到大量的内核态用户态切换, 很容易引发线程的调度.
轻量级锁 : 涉及到少量的内核态用户态切换., 不太容易引发线程调度.
总结
重量级锁, 加锁解锁过程更慢, 更低效.
轻量级锁, 加锁解锁过程更快, 更高效.
注 :
一个乐观锁很可能是个轻量级锁.
一个悲观锁很可能是个重量级锁.
自旋锁和挂起等待锁
自旋锁
产生锁冲突时,线程在抢锁失败后会进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
伪代码 :
while (抢锁(lock) == 失败) {}
如果获取锁失败, 会立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.一旦锁被其他线程释放, 就能第一时间获取到锁.
挂起等待锁
与自旋锁不同, 挂起等待锁获取锁失败后 不会再次尝试获取锁, 而是等系统再次调度到了, 再尝试获取锁.
总结
自旋锁是轻量级锁的一种典型实现, 通常是纯用户态的, 不需要经过内核态.(时间相对更短)
挂起等待锁是重量级锁的一种典型实现, 通过内核的机制来实现挂起等待.(时间更长)
synchronized 的锁策略
synchronized 即是乐观锁, 也是悲观锁, 即是轻量级锁, 也是重量级锁, 轻量级锁部分基于自旋锁实现, 重量级锁部分基于挂起等待锁实现.
synchronized 会根据当前锁竞争的激烈程度, 进行自适应.
如果锁冲突不激烈, 则以轻量级锁 / 乐观锁状态运行.
反之, 以重量级锁 / 悲观锁状态运行.
互斥锁和读写锁
synchronized 只有两个操作 :
- 进入代码块, 加锁
- 出代码块. 解锁.
synchronized 的加锁没有更细致的划分, 它是互斥锁.
注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.
因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.
读写锁就是减少"互斥"来提高效率, 他能把读和写两种加锁区分开来.
读写锁:
- 给读加锁
- 给写加锁
- 解锁
读写锁中规定 :
- 读锁和读锁之间, 不会产生锁竞争, 不会产生阻塞等待.
- 写锁与写锁之间, 会产生锁竞争.
- 写锁与读锁之间, 会产生锁竞争.
读写锁是非必要不加锁, 更适合一写多读的情况.
Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁, 提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁, 提供了 lock / unlock 方法进行加锁解锁.
可重入锁与不可重入锁
在一个线程中, 如果对同一个锁对象连续加锁两次, 且不死锁, 就叫做可重入锁, 如果死锁了, 就叫不可重入锁.
来段伪代码来理解下 :
synchronized(lock) { synchronized(lock) { } }
这个代码便是加锁两次, 第二次加锁需要等待第一个锁释放, 第一个锁所释放需要第二次加锁成功 并走完代码块, 逻辑上产生了矛盾,产生了死锁.
实际上上述代码并不会死锁, 因为 synchronized 是可重入锁, 那对于不可重入锁, 上述情况就会产生死锁.