锁策略, cas 和 synchronized 优化过程
一、锁策略
1、乐观锁和悲观锁
乐观锁:预测一般情况下不会发生锁冲突,后续做的工作更少
悲观锁:预测一般情况下会发成锁冲突,后续做的工作更多
2、读写锁
对读操作或写操作加锁,读加锁和写加锁之间不互斥。Synchronized 不是读写锁。Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁:
- ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。
- ReentrantReadWriteLock.WriteLock 类表示一个写锁。这个对象也提供了 lock / unlock 方法进行加锁解锁。
3、重量级锁和轻量级锁
重量级锁:加锁的开销大,占用系统资源少,适合锁被其他线程持有时间久的情况。
轻量级锁:加锁的开销小,占用系统资源多,适合锁被其他线程持有时间短的情况。
4、自旋锁和挂起等待锁
自旋锁:轻量级锁的一种典型实现。在用户态下,通过自旋的方式(while循环),实现类似于加锁的效果,消耗一定的CPU资源,但可最快速度拿到锁。
挂起等待锁:重量级锁的一种典型实现。通过内核态,借助系统提供的锁机制,当出现锁冲突时,会牵扯到内核对于线程的调度,使冲突的线程出现挂起(阻塞等待),消耗的CPU资源少,但无法保证第一时间拿到锁。
5、公平锁和非公平锁
公平锁:先来后到。
非公平锁:每个线程获得锁的概率相等,虽然看起来公平,但是实际不公平(每个线程的阻塞时间不一样)。
synchronized 是非公平锁
6、可重入锁和不可重入锁
可重入锁:允许同一个线程多次获取同一把锁。
不可重入锁:当一个线程已经获得了一把锁,当第二次尝试加同样的锁时,就会进入阻塞等待,直到第一次的锁释放,但是释放第一个锁也需要该线程来完成,这时就发生了死锁。这样的锁就被成为不可重入锁。
synchronized 是可重入锁。
二、CAS
CAS(Compare and swap),字面意思“比较并交换”。CAS本质上是一种无锁编程,CAS 还是乐观锁的一种实现方式。
涉及的操作:(我们假设内存中的原数据V,旧的预期值A,需要修改的新值B)
比较 A 与 V 是否相等。(比较) 如果比较相等,将 B 写入 V。(交换) 返回操作是否成功。
// CAS 伪代码
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}// CAS 伪代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的
// 伪代码只是帮助我们理解 CAS 的工作流程
1、CAS 实现原子类
标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类,其中的 getAndIncrement 相当于 i++ 操作。
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
注意:
- CAS 是直接读写内存的,而不是操作寄存器。
- CAS 的读内存、比较、写内存操作时一条硬件指令,是原子的。
- 本来 check and set 这样的操作在代码角度不是原子的,但在硬件层面上可以让一条指令完成这样的操作,也就变成原子的了。
2、实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权。
// 自旋锁伪代码
public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
3、ABA 问题
问题描述:CAS是根据判断内存和寄存器中的值是否相等来进行判断其是否发生改变。但是如果这两者都发生了从A变成B,又从B变成了A的情况,那我们的CAS就会判断错误,从而导致A再次发生变化。
经典转账案例:
- 我账户上有100元;
- 我在 ATM1 转账100给女朋友;
- 由于 ATM1 出现了网络问题,卡住了,这是我跑到旁边的 ATM2 再次操作转账;
- ATM2 马上执行了 CAS(100,0),完成了转账操作,此时我的账户余额变成了0;
- 老爸这时又给我账上转了100,此时我的账户余额变成了100;
- 这是 ATM1 网络回复,继续执行 CAS(100,0),居然执行成功了,我的账户余额又变成了0;
- 这时候收到老爸的微信,说给我转了100,问我收到了没有。我查了一下张,摇了摇头,一脸懵逼。
解决方法:给要修改的值,引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。