什么是CAS?Compare and swap :比较和交换
一个CAS操作涉及:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
CAS实现原子类;例如:Java标准库提供的原子类AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();//相当于++操作;多线程在++操作就是安全的
假设在多线程进行这个++操作:伪代码
执行过程:
1:先把内容读取到自己的栈内存上(cpu寄存器)
2:假设线程1先执行CAS操作.由于oldValue和value的值相同,直接进行对value赋值。CAS是直接读写内存的,而不是操作寄存器;CAS的读内存,比较,写内存操作是一条硬件指令,是原子性。
3::写完后的结果如下
4:线程2再执行CAS操作,第一次CAS的时候发现写入失败;因为oldValue和value不相等。因此需要进入循环;在循环里重新读取value的值赋给oldValue。重复上述流程
上述使用一个原子类.不需要使用重量级锁,就可以高效的完成多线程的自增操作.
伪代码:
通过 CAS 操作检查当前锁是否被其他线程持有。如果锁已经被持有,CAS 操作会失败,进入自旋等待状态。
如果锁没有被其他线程持有,CAS 操作成功,将当前线程设为锁的持有者(owner)。
这是一个典型的自旋锁实现,会一直循环检测直到成功获取锁。
public class SpinLock {
private Thread owner = null;
public void lock() {
// 通过CAS(Compare and Swap)检查当前锁是否被某个线程持有。
// 如果这个锁已经被其他线程持有,则自旋等待。
// 如果这个锁没有被其他线程持有,则将owner设为当前尝试加锁的线程。
while (!CAS(this.owner, null, Thread.currentThread())) {
// 自旋等待,直到成功获取锁
}
}
public void unlock() {
// 释放锁,将owner设为null
this.owner = null;
}
}
CAS 在运行中的核心,检查 value 和 oldvalue 是否一致如果一致;就视为 vaue 中途没有被修改过,所以进行下一步交换操作是没问题的。
上述的一致有两种可能:这里 一致,可能是没改过。也可能是,改过,但是还原回来了(ABA问题)。
ABA问题是特殊情况;但是仍然是致命的;假设有CAS方式扣款;判断这个钱扣了没;你银行卡有1000块钱要取500。
现在两个线程读到你余额是1000情况;然后你取的时候有人给你转了500。这会就会线程1这个扣完;然后线程3又充500;线程2接着扣。
怎么解决呢?
想象成,初始版本号是 1,每次修改版本号都 + 1然后进行 CAS 的时候不是以金额为基准了,而是以版本号为基准。版本号只能升;不能降;只要版本号没变就一定没发生改变;这个问题得我们自己去避免。基于版本号实现的乐观锁是一种典型的实现方式。
jdk1.8下
synchronized (locker) { }过程:
1:无锁状态;初始阶段,对象的标记为无锁状态。当第一个线程进入synchronized代码块时,它会尝试获取锁,此时这个对象的标记变为偏向锁。
2:偏向锁;进行加锁的时候,首先先会进入到偏向锁状态;偏向锁,并不是真正的加锁,而只是占个位置;有需要再真加锁,如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)。跟做个标记一样;然后清空标记。
3:轻量级锁状态: 当发生锁竞争,偏向锁会升级为轻量级锁。synchronized相当于自旋的方式加锁(跟上述CAS伪代码逻辑一样),并尝试使用CAS来争夺锁。
4:重量级锁状态: 如果要是很快别人就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋,并不划算.synchronized 自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成 重量级锁(挂起等待锁;)
可以想象是有个计数器记录循环多少次;循环多久;然后到一定程度结束循环执行重量级锁的逻辑;让其先放弃cpu;当前线程一旦被切换cpu就是比较低效的事情;因为即使对方释放锁了;你这边也得要等待到你调度的时候;谁能保证猴年马月。
注意:锁只能这样子升级;不会降级;重入的情况呢?synchronized对重入的情况是特殊处理;不加锁;只是计数。
编译器智能的判定,看当前的代码是否是真的要加锁,如果这个场景不需要加锁,程序猿也加了,就自动把锁给去掉。
比如:StringBuffer sb = new StringBuffer();是线程安全的;底层是synchronized保证的。但是单线程你并不会用到线程安全问题;所以就是我们使用StringBuffer和StringBudder通常混用不区分。
sb.append(“a”);
sb.append(“b”);
sb.append(“c”);
sb.append(“d”);
锁的粒度: synchronized 包含的代码越多,粒度就越粗包含的代码越少,粒度就越细。有时候并不是锁的粒度越细越好。
一般情况下认为是锁的粒度细一点好;这样子并发的块就比较多;但是有些两次加锁解锁之间,间隙非常小此时莫不如就直接一次大锁好;因为反复的加锁和解锁会消耗更多资源。(间隙特别小;就算并发也作用不大;并发减少的时间;不如加锁的开销大。)