|
JavaEE
JavaEE——No.2 多线程案例
JavaEE——常见的锁策略
CAS: 全称Compare and swap,字面意思: ”比较并交换“. 这是由 CPU 的一条指令, 是原子的.
一个 CAS
涉及到以下操作:
# 注意 #
该操作不关心交换之后 B
的值, 更关心交换之后 M
的值, 此处的交换相当于是把 B 赋值给 M 了.
这是一条由 CPU
的命令, 原子完成的, 是线程安全的 ! ! 效率很高 ! !
CAS 伪代码
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
当多个线程同时对某个资源进行 CAS
操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。
在之前, 使用多线程进行, count++
操作是线程不安全的, 如果想要安全, 就需要加锁, 而加了锁性能就大打折扣.
这时, 我们就可以基于 CAS 操作来实现 原子
的 count++. 从而保证线程的安全和高效.
伪代码
//标准库中已经封装好的一个类
class AtomicInteger {
private int value;
//相当于 count++
public int getAndIncrement() {
// oldValue 相当于是 寄存器A, 相当于把 内存value 的值读到寄存器里
int oldValue = value;
// 此处的 oldValue + 1 相当于是另一个 寄存器B 的值
while ( CAS(value, oldValue, oldValue+1) != true) {
//比较 value 这个内存中的值, 是否和 寄存器A 的值相同
oldValue = value;
}
return oldValue;
}
}
while( ) 解释
value
这个内存中的值, 是否和 寄存器A
的值相同.寄存器B
中的值, 设置到 value
中, 同时 CAS 返回 true, 结束循环.false
, 进入循环, 循环体里面重新读取 内存value 的值到寄存器A 中.为什么是线程安全的 ?
假设 线程t1 和 线程t2 同时调用 getAndIncrement()
方法. 且执行顺序如下.
如上操作未涉及到挂起等待. 这样的操作, 比等待锁操作, 锁需要的开销低非常多
自旋锁, 是纯用户态的轻量级锁, 当发现锁被其他线程持有的时候, 另外的线程不会挂起等待, 而是会反复询问, 看当前的锁是否被释放了
伪代码
public class SpinLock {
//一个线程示实例, 表示当前这把锁是谁获取到的, null 表示锁的无人获取的状态
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
while( ) 解释
owner
和 null
是否相同lock
的线程的值, 设置到 owner 中(相当于加锁成功), 同时结束循环false
, 再次进入循环, 又会立即再次发起判定在 CAS
中, 进行比较的时候, 你发现寄存器 A 和内存 M 的值相同, 但你无法判定是 M 始终没变, 还是 M 变了, 又变回来了.
这就好比, 我们买一个手机, 无法判定这个手机是刚出厂的新手机, 还是别人用旧了, 又翻新过的手机.
假设 gujiu, 有 1000
存款, 有一天, 她想从 ATM 中取出 500 (ATM 扣款是基于 CAS
来完成的).
恰好在取钱的时候, 卡了以下, gujiu 连按了几下取钱, ATM 创建出了 两个线程
, 来进行扣款操作.
那我们如何解决 ABA 问题呢 ?
只需要有一个记录, 能够记录到 内存 中数据的变化, 就可以解决 ABA
问题了.
另外搞一个内存记录版本号, 保存 M
的 “修改次数”[只增不减] , 或者是 “上次修改时间”[只增不减].此时修改操作, 就不是读账户余额了, 而是读内存中的版本号, 且比较的也是的版本号.
|
以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!