锁策略和程序员无关,和"实现锁"的人才有关系
所提及到的锁策略,和Java本身没有关系,适用于所有和"锁"相关的情况.
悲观锁:预期所冲突的概率很高
乐观锁:预期锁冲突的概率很低
悲观锁做的工作更多,付出的成本更多,更低效
乐观锁做的工作更多,付出的成本更低,更高效
对于普通的互斥锁,只有两个操作 加锁和解锁
只要两个线程针对同一个对象加锁,就会产生互斥
对于读写锁来说,分为了三个操作
- 加读锁 : 如果代码只是进行了读操作,就加读锁
- 加写锁 : 如果代码只是进行了写操作,就加写锁
- 解锁 : 针对上面两个锁都可以解锁
针对读写锁:
重量级锁,就是做的事情比较多,开销更大
轻量级锁,就是做的事情比较少,开销更小
在一般情况下,不绝对的情况下
悲观锁可以看做是一个重量级锁
乐观锁可以看做是一个轻量级锁
再使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口)此时一般认为是重量级锁(操作系统的锁会在内核做很多的事情,比如线程等待…)
如果锁是纯用户态实现的,此时一般认为是轻量级锁(用户态的代码更可控,更高效)
挂起等待锁 , 往往就是通过内核的一些机制来实现的,往往较重 (重量级锁的一种典型实现)
自旋锁 , 往往就是通过用户态代码实现的 (轻量级锁的一种典型实现)
公平锁:多线程在等待同一把锁的时候,谁是先来的,谁可以获得这把锁(先来后到原则)
非公平锁: 多线程在等待同一把锁的时候,不遵循先来后到(每个等待的线程获取到锁的概率是均等的)
对于操作系统来说,本身线程之间的调度是随机的(均等的) 操作系统提供的 mutex 这个锁就是非公平锁
要想实现公平锁,反而需要付出更多的代价
一个线程针对同一个锁,连续加锁两次,如果会死锁,就是不可重入锁;如果不会发生死锁,就是可重入锁
- 既是一个乐观锁,也是一个悲观锁(根据锁的激励竞争,自适应)
- 不是读写锁,只是一个普通互斥锁
- 既是一个轻量级锁,也是一个重量级锁(根据锁的激励竞争,自适应)
- 轻量级锁的部分基于自旋锁来实现,重量级锁的部分基于挂起等待锁来实现
- 非公平锁
- 可重入锁
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。(交换)
返回操作是否成功。
CAS伪代码
下面写的代码不是原子的, 真实的 CAS 是CPU提供的单独的CAS指令通过这一指令,就可以完成下面伪代码的过程,是原子的,所以线程安全
这个伪代码只是辅助理解CAS 的工作流程.
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
address
: 待比较的内存地址
expectValue
: 预期内存的值
swapValue
:希望将内存的值改成这个新的值代码拿出待比较的内存空间地址的值和预期内存的值进行比较,如果相同,就把期望更改的新的 值写入到内存地址中,并且返回true;如果比较结果不相同,返回false.
CAS最大的意义就是为我们写多线程安全的代码,提供了新的思路和方向
Java标准库中提供了一组原子类,针对所常用的依稀int,long.int array…进行一封装,可以基于CAS的方式进行修改,并且线程安全
这个代码里面不存在线程安全问题,他是基于CAS实现的++操作,既可以保证线程安全,可比synchronized高效,synchronized会涉及锁的竞争,两个线程相互等待,CAS不会涉及这些.是工作中高频使用更多东西
伪代码实现
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
value存储的是原始的数据
int oldValue = value;
操作看起来是一个oldValue
变量实际可能是一个寄存器(伪代码不好表示寄存器) 这个操作相当于从内存中读到寄存器中(load)
CAS(value, oldValue, oldValue+1)
判定一下当前内存的值是不是和刚才寄存器读到的值一致,如果判定成功,就把value的值改为oldValue+1
,返回true,循环结束,如果判定失败,就啥也不做,返回false,继续下次循环(下次循环,先重新读一下value,在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;
}
}
owner 表示的是自旋锁,记录的是当前的锁被哪个线程持有,如果为null表示该当前未加锁\
while(!CAS(this.owner, null, Thread.currentThread()))
通过一个循环来实现循环里调用CAS,CAS会比较前面的owner值是否为null,如果是null就改成当前线程,意思就是当前线程拿到了锁;如果不是null,就返回false进入下一次循环,下次循环依旧是CAS操作如果当前这个锁一直被别人持有,当前尝试加锁的线程就会在这个循环里快速反复的进行循环---->自旋
自旋锁是一个轻量级锁,可以看做是一个乐观锁
CAS的关键就是 先比较 在交换
比较其实是在比较当前值 和旧值是不是相同,把这两个值相同,就是视为中间没有发生过改变.
但是这个中间没有发生改变也有可能是中间改变过了,然后又变回来了
例如一开始是A,中间变成了B,最后又变回成了A,这是比较的时候,以为没有发生改变,但其实发生过改变.
假如我有的账户有100元,这时我去ATM机准备取50元,但当我按下取款按钮的时候,机器卡了,我就多按了一下取款按钮.这相当于一次取钱操作,执行了两次(两个线程,并发的去执行这个操作),期望的是只取成功一次
基于CAS对的方式实现这里的取款操作
ind oldValue = value;//读取旧值
CAS(&value,oldValue,oldValue-50)
此时没有发生ABA问题,但是加一个条件,就是朋友这个时候转过来了50元钱,就会发生ABA
可以看到,在执行t2的cas
操作之前,value的值和oldValue
的值相同,所以还要继续扣50,这就产生了安全问题.
给要修改的值, 引入版本号. 这个版本号只能变大,不能变小,修改变量的时候,比较的不是变量本身,而是比较版本号.
CAS 操作在读取旧值的同时, 也要读取版本号.
上述例子:
ATM的余额引入一个版本号
一开始的版本号为一,t1线程cas后的版本号为2,t3转账后的版本号编程了3,t2线程在进行cas时,比较版本号会发现不相同,不做操作
不光是可以插入版本号,只要是一个单调递增或者单调递减的数据就可以,比如时间戳