目录
new AtomicInteger().getAndIncrement()流程
如何解决?:通过AtomicStampedReference版本号
没有CAS之前
常用synchronized
锁保证线程安全i++,但是它比较重 ,牵扯到了用户态和内核态的切换,效率不高。
使用CAS之后
类似于乐观锁保证线程安全i++
CAS是什么
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。否则进行自选重新获取。
CAS底层原理:unsafe类
compareAndSet调用的是unsafe中的本地方法
Unsafe
CAS这个理念 ,落地就是Unsafe类
- 注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源故行相应任务
它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门 ,基于该类可以直接操作特定内存\ 的数据 。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
变量valueOffset
,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
变量value用volatile修饰,保证可见性
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
new AtomicInteger().getAndIncrement()流程
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
1 AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
2 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
3 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
4 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
5 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS缺点
1 循环时间长开销很大
do while
如果它一直自旋会一直占用CPU时间,造成较大的开销
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2 ABA问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类可能会有数据的变化。
- 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,
- 然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
- 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
ABA代码演示
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
new Thread(()->{
atomicInteger.compareAndSet(100,2022);
try { TimeUnit.MICROSECONDS.sleep(200); } catch (InterruptedException e) {e.printStackTrace();}
atomicInteger.compareAndSet(2022,100);
},"t1").start();
new Thread(()->{
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace();}
System.out.println(atomicInteger.compareAndSet(100, 2000)+"\t"+atomicInteger.get());
},"t2").start();
}
}
//true 2000
如何解决?:通过AtomicStampedReference版本号
public boolean weakCompareAndSet(V expectedReference,//旧值
V newReference,//新值
int expectedStamp,//旧版本号
int newStamp)//新版本号
以原子方式设置该引用和邮票给定的更新值的值,如果当前的参考是==至预期的参考,并且当前标志等于预期标志。
代码演示
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//初始版本号
System.out.println(Thread.currentThread().getName() + "的首次版本号:"+stamp);
try { TimeUnit.MICROSECONDS.sleep(200); } catch (InterruptedException e) {e.printStackTrace();}
boolean b2 = atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(b2+"\t"+Thread.currentThread().getName() + "的2次版本号:"+atomicStampedReference.getStamp() );
boolean b3 = atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(b3+"\t"+Thread.currentThread().getName() + "的3次版本号:"+atomicStampedReference.getStamp() );
},"t1").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//初始版本号
System.out.println(Thread.currentThread().getName() + "的首次版本号:"+atomicStampedReference.getStamp());
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) {e.printStackTrace();}
boolean b = atomicStampedReference.compareAndSet(100, 2000, stamp, stamp + 1);
System.out.println(b+"\t"+Thread.currentThread().getName() + "的2次版本号"+atomicStampedReference.getStamp());
},"t2").start();
}
t1的首次版本号:1
t2的首次版本号:1
true t1的2次版本号:2
true t1的3次版本号:3
false t2的2次版本号3
3不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
自定义原子引用
代码实现
public class Automic {
public static void main(String[] args) {
User z3 = new User("z3", 19);
User li4 = new User("li4", 22);
new AtomicInteger().getAndIncrement();
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
@AllArgsConstructor
@ToString
@Getter
@Setter
class User{
String name;
int age;
}
true User(name=li4, age=22)
false User(name=li4, age=22)
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,
当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
优点:减少线程上下文切换的消耗,
缺点:循环会消耗CPU。
通过CAS实现手自旋锁
自旋锁好处:循环比较获取没有类似wait的阻塞。
- 通过CAS操作完成自旋锁,t1线程先进来调用myLock方法自己持有锁5秒钟,t2随后进来后发现t1线程持有锁,不是null,所以循环自旋等待,直到t1释放锁后t2随后抢到。
利用AtomicReference.compareAndSet
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"--come in");
while (!atomicReference.compareAndSet(null, thread)){//不能修改就一直获取 修改成功不进入循环
}
}
public void unlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName()+"--unlock");
}
public static void main(String[] args) {
SpinLockDemo lockDemo = new SpinLockDemo();
new Thread(()->{
lockDemo.lock();
System.out.println("等待3s");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockDemo.unlock();
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
lockDemo.lock();
lockDemo.unlock();
},"t2").start();
}
}
t1--come in
等待3s
t2--come in
t1--unlock
t2--unlock