在并发编程中我们都知道 i++ 操作是非线程安全的,这是因为 i++ 操作不是原子操作。
如何保证原子性呢?常用的方法就是加锁。 在Java语言中可以使用 synchronized 和CAS 实现加锁效果。
synchronized 是悲观锁,线程开始执行的第一步就是获取锁,一旦获取锁,其他线程进入后就会阻塞等待锁,而后在争夺到锁资源后恢复为RunNable状态,这个过程涉及到操作系统用户模式和内核模式的转换,代价比较高。
CAS 是乐观锁,线程执行的时候不会加锁,假设没有冲突去完成某项操作,如果因为冲突失败了就重试,最后直到成功为止。
CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new) 比较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。
所以这里的预期值E本质上指的是“旧值”。
举例:
1.如果有一个多个线程共享的变量 i 原本等于 5,现在在线程A中,想把它设置为新的值6;
2.使用CAS来做这个事情
3.首先用 i 去与 5 对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,
i 的值被设置成了6;
4.如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍
然为2。
在这个例子中,i就是V,5就是E,6就是N。
那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?
不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
CAS是一种原子操作。那么Java是怎样来使用CAS的呢?我们知道,在Java方法中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM 使用c或者c++去实现。
在Java中,有一个Unsafe类,它在 sun.misc 包中,它里面是一些 native 方法,其中就有几个关于 CAS 的:
boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
当然,他们都是 public native的。
Unsafe中对 CAS 的实现是C++ 写的,它的具体实现和操作系统、CPU都有关系
Linux的X86下主要通过 cmpxchgl 这个指令在CPU 级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。
Unsafe类里面还有其他方法用于不同的用途。比如支持线程挂起和恢复的 park 和 unpark, LockSupper类底层就是调用了这两个方法。还有支持反射操作的 allocateInstance()方法。
CAS 多与自旋结合,如果自旋CAS 长时间不成功,会占用大量的 CPU资源。
例:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
解决思路是让JVM 支持处理器提供的 pause 指令
pause 指令能让自旋失败时cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,未解决内存顺序冲突而导致 的CPU 流水线重排 的代价也会小很多。
有两个线程,线程A去获得一个临界区的锁,而这个锁正被线程B锁持有
互斥锁:线程A会被阻塞
自旋锁:一直循环等待,进行锁请求,直到获得这个锁为止。
自旋锁的作用是为了解决某项资源的互斥使用,因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
不足之处:自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短时间内获得锁,会使CPU效率降低
详情参考至一缕阳光a的博客
解决方案:
1.使用JDK1.5开始就提供的AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作;
2.使用锁。锁内的临界区代码可以保证只有当前线程能操作。
例:
假设小明要到银行取钱,此时账户余额为100元,小明取款50,由于硬件出问题,取款操作被提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元,理想情况下应该是一个线程更新成功一个失败,小明的存款值被扣一次。
线程1:获取当前值100,期望更新为 50
线程2:获取当前值100,期望更新为50
线程1 首先执行成功,把余额修改为50元,线程2 因为某种原因阻塞。这时,小明的妈妈刚好给小明汇款50元
线程1:获取当前值100,成功更新为50
线程2:获取当前值100,希望更新为50,BLOCK
线程3:获取当前值50,希望更新为 100
线程2 仍然是阻塞状态,线程3执行成功,把余额从50改为100
线程1:获取当前值100,成功更新为50,已返回
线程2:获取当前值100,希望更新为50,BLOCK
线程3:获取当前值50,成功更新为 100
线程2恢复运行,由于阻塞之前获得了“当前值“100,并且经过 compare检测,此时存款值也是100,所以会成功把变量值100更新为50
线程1:获取当前值100,成功更新为50,已返回
线程2:获取当前值100,希望更新为50,成功更新为50
线程3:获取当前值50,成功更新为 100,已返回
原本线程2 应当提交失败,小明的正确余额应该为100元,结果由于ABA问题提交成功了。
解决方法:
加版本号或者时间戳
从JDK1.5开始,JDK的atomic包里提供了一个类 AtomicStampedRefernce 类来解决ABA 问题
这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}