1 锁的由来
1.1 并发
并发其实很简单,并发就是多线程并发访问,就是有N多个线程都来获取相同的资源,
1.2 串行化
我们说的线程安全也就是多线程并发访问资源时,保证其有序性,那么如何来保证有序性呢?
1.3 锁
锁就是保证并发有序性的一种手段。及加锁可以实现线程安全。
2 那么锁该怎么加呢?
加锁时候其实就需要去考虑场景了
2.1 乐观锁 读多写少
乐观锁本质是不加锁,我们认为遇到并发写的可能性会很低,每次拿到的数据我们认为是不会被修改的,所以是不会阻塞的,但是为了保证数据的一致性,我们在并发写的时候需要再次确认一下,拿到的是不是最新的数据。如果不是,再去取一份,然后再次验证,直到验证通过后,才可以写入。
2.2 被观锁 读少写多
悲观锁就是认为写多,读少,每次读写数据时都会上锁,让其他线程想要获取资源的线程进入阻塞状态,直到当前获取到资源的线程释放资源,下一个线程才能获取到资源。
3 锁的实现
3.1 乐观锁
Java中常用的乐观锁就是CAS算法+自旋(需要考虑ABA问题,CPU性能)。
适应性自旋锁(是JVM对自旋的一种优化,会选择一个最佳时间进行自旋,而不是一直保持自旋消耗CPU资源)
实现:
// 乐观锁
public class AtomicInteger extends Number implements Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
JDK1.5开始atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
也有一些是基于版本号解决ABA问题,版本号只增不减。
3.2 悲观锁
对于Java来说,synchronized关键字和Lock的实现类都是悲观锁。
RetreenLock的原理是基于AQS,AQS是先尝试使用CAS自旋,如果获取不到就会转化为悲观锁。
悲观锁往往是需要通过阻塞线程实现的,想要更为深刻的理解悲观锁,我们就需要知道线程的各个状态。
线程的状态,如下图所示:
- 线程A 访问资源,发现此时没有锁
- 线程A 获取到资源,锁住资源,不允许其他线程操作
- 线程B、C...访问资源,发现资源被锁,然后B、C...进入阻塞状态
- 当线程A操作完成,释放资源的锁
此时B、C会进入就绪状态进行竞争,能力强的得到锁(这里就引入公平锁【按先后顺序获取锁】、非公平锁的概念【强势的获取到锁】)在锁竞争时,就可能出现死锁的情况。
锁竞争又会带出优化锁的知识点【
- 降低锁的粒度
- 减少持有锁的时间
- 锁分离(读写锁分离)
- 锁升级(对加锁的一种优化操作)】
- 线程B 获取到锁,线程状态重新恢复到运行态,未获取到锁的继续恢复阻塞,等待时机
- ...