常见锁策略
锁策略和程序员无关,和"实现锁"的人才有关系
所提及到的锁策略,和Java本身没有关系,适用于所有和"锁"相关的情况.
悲观锁 vs 乐观锁(处理锁冲突的原因)
悲观锁:预期所冲突的概率很高
乐观锁:预期锁冲突的概率很低
读写锁 vs普通的互斥锁
对于普通的互斥锁,只有两个操作 加锁和解锁
只要两个线程针对同一个对象加锁,就会产生互斥
针对读写锁:
- 针对读锁和读锁之间,是不存在互斥的(多线程同时同一个变量,不会有线程安全问题)
- 针对读锁和写作之间,存在互斥
- 针对写锁和写锁之间,存在互斥
重量级锁 vs轻量级锁(处理所冲突的结果)
重量级锁,就是做的事情比较多,开销更大
轻量级锁,就是做的事情比较少,开销更小
在一般情况下,不绝对的情况下
再使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口)此时一般认为是重量级锁(操作系统的锁会在内核做很多的事情,比如线程等待…)
如果锁是纯用户态实现的,此时一般认为是轻量级锁(用户态的代码更可控,更高效)
挂起等待锁 vs 自旋锁
挂起等待锁 , 往往就是通过内核的一些机制来实现的,往往较重 (重量级锁的一种典型实现)
自旋锁 , 往往就是通过用户态代码实现的 (轻量级锁的一种典型实现)
公平锁 vs非公平锁
公平锁:多线程在等待同一把锁的时候,谁是先来的,谁可以获得这把锁(先来后到原则)
非公平锁: 多线程在等待同一把锁的时候,不遵循先来后到(每个等待的线程获取到锁的概率是均等的)
可重入锁 vs 不可重入锁
一个线程针对同一个锁,连续加锁两次,如果会死锁,就是不可重入锁;如果不会发生死锁,就是可重入锁
synchronized的锁策略
CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
CAS伪代码
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
CAS最大的意义就是为我们写多线程安全的代码,提供了新的思路和方向
CAS的应用
实现原子类
Java标准库中提供了一组原子类,针对所常用的依稀int,long.int array…进行一封装,可以基于CAS的方式进行修改,并且线程安全
伪代码实现
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
实现简图:
基于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;
}
}
CAS的ABA问题(重要)
CAS的关键就是 先比较 在交换
比较其实是在比较当前值 和旧值是不是相同,把这两个值相同,就是视为中间没有发生过改变.
关于ABA问题的实例
假如我有的账户有100元,这时我去ATM机准备取50元,但当我按下取款按钮的时候,机器卡了,我就多按了一下取款按钮.这相当于一次取钱操作,执行了两次(两个线程,并发的去执行这个操作),期望的是只取成功一次
基于CAS对的方式实现这里的取款操作
ind oldValue = value;//读取旧值
CAS(&value,oldValue,oldValue-50)
此时没有发生ABA问题,但是加一个条件,就是朋友这个时候转过来了50元钱,就会发生ABA
可以看到,在执行t2的cas
操作之前,value的值和oldValue
的值相同,所以还要继续扣50,这就产生了安全问题.
解决方案
给要修改的值, 引入版本号. 这个版本号只能变大,不能变小,修改变量的时候,比较的不是变量本身,而是比较版本号.
CAS 操作在读取旧值的同时, 也要读取版本号.
- 真正修改的时候,
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
- 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
不光是可以插入版本号,只要是一个单调递增或者单调递减的数据就可以,比如时间戳