CAS(理解)AtomicInteger(源码)
首先理解悲观锁和乐观锁:
乐观锁:
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。(有点像自旋锁)
CAS主要是三个操作:
1.获取当前值
2.当前值加一赋给目标值
3.进行CAS操作,成功跳出循环,失败就重复上述操作
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger ai = new AtomicInteger(); private int i = 0; public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<Thread>(); // 添加100个线程 for (int j = 0; j < 100; j++) { ts.add(new Thread(new Runnable() { public void run() { // 执行100次计算,预期结果应该是10000 for (int i = 0; i < 100; i++) { cas.count(); cas.safeCount(); } } })); } //开始执行 for (Thread t : ts) { t.start(); } // 等待所有线程执行完成 for (Thread t : ts) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("非线程安全计数结果:"+cas.i); System.out.println("线程安全计数结果:"+cas.ai.get()); } /** 使用CAS实现线程安全计数器 */ private void safeCount() { for (;;) { int i = ai.get(); // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值
if (ai.compareAndSet(i, ++i)) {
System.out.println(i);//并不是按顺序输出的,但所有的值都会输出来
break;
}
} } /** 非线程安全计数器 */ private void count() { i++; } } //结果: 非线程安全计数结果:9867 线程安全计数结果:10000
CAS操作中的三个问题:
1.ABA问题:
例子:
小灰有100元存款,要用一个提款机来提款50元。
由于提款机硬件出现问题,小灰提款操作被提交两次,开启两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下是一个线程更新成功一个线程更新失败,只扣一次。
线程1(提款机): 获取当前值100元,成功更新成50元
线程2(提款机): 获取当前值100元,期望更新为50,BLOCK
线程3(小灰妈): 获取当前值50元,期望更新为100元
在线程1和3执行完了以后,compare以后线程2会再次执行,就会执行两次扣钱。
如何解决这个问题?
除了比较期望值以外还要比较变量的版本号,在线程1操作的时候期望值100,版本号a01,在线程3操作完以后,期望值虽然还是变成了100,但是版本号变为了a03,在线程2去操作的时候发现版本号不一致了(之前保存的版本号是a01),就不会执行这个操作了
在Java底层源码里面有一个类AtomicStampedReference类就是解决ABA问题的 ,但它保存的是每次修改数据的数据段额时间戳,在每次修改的时候出来比较原始数据是否相等以外,还要比较时间戳。
参考:https://www.sohu.com/a/215510186_465221
2.循环时间开销大的问题:
观察上面的代码,如果在循环的过程中长时间无法成功不能退出,那么会给cpu带来非常大的执行开销
3.只能保证一个共享变量的操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
参考:https://286.iteye.com/blog/2295165
引申:关于总线锁和缓存锁的理解?
AtomicInteger的底层源码实现:通过Unsafe这个类,计算实例对象在内存中的偏移量
// 使用 unsafe 类的原子操作方式 private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { //计算变量 value 在类对象中的偏移量 valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
Unsafe内部实现:
//volatile变量value private volatile int value; /** * 创建具有给定初始值的新 AtomicInteger * * @param initialValue 初始值 */ public AtomicInteger(int initialValue) { value = initialValue; } //返回当前的值 public final int get() { return value; } //原子更新为新值并返回旧值 public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //最终会设置成新值 public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue); } //如果输入的值等于预期值,则以原子方式更新为新值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //方法相当于原子性的 ++i public final int getAndIncrement() { //三个参数,1、当前的实例 2、value实例变量的偏移量 3、递增的值。 return unsafe.getAndAddInt(this, valueOffset, 1); } //方法相当于原子性的 --i public final int getAndDecrement() { //三个参数,1、当前的实例 2、value实例变量的偏移量 3、递减的值。 return unsafe.getAndAddInt(this, valueOffset, -1); }
Unsafe类可以执行以下几种操作:
- 分配内存,释放内存:在方法allocateMemory,reallocateMemory,freeMemory中,有点类似c中的malloc,free方法
- 可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法(这里我们主要使用这个功能)
- 挂起和恢复线程,被封装在LockSupport类中供使用
- CAS操作(CompareAndSwap,比较并交换,是一个原子操作)
本文来自博客园,作者:LeeJuly,转载请注明原文链接:https://www.cnblogs.com/peterleee/p/10478881.html