文章目录
- 1、初体验
- 2、CAS概述
- 3、Unsafe类
- 4、Unsafe汇编
- 5、原子引用AutomicReference
- 6、手写自旋锁SpinLock
- 7、CAS的两大缺点
- 8、AtomicStampedReference类解决ABA问题
1、初体验
没有CAS时,多线程环境下不使用原子类保证线程安全,比如i++,可以synchronized搭配volatile:
public class Counter{
private volatile int vlaue = 0; //volatile
public int getValue(){ //利用volatile可见性保证并发下也能读取到最新值
return value;
}
public synchronized int incerment(){ //利用synchronized保证复合操作的原子性
return value++;
}
}
使用CAS,多线程下使用原子类保证线程安全,还是进行i++:
AtomicInteger atomicInteger = new AtomicInteger();
public int getAtomicInteger(){
return atomicInteger.get();
}
public int setAtomicInteger(){
return atomicInteger.getAndIncrement(); //i++
}
Atomiclnteger 类主要利用 CAS (compare and swap)+ volatle 和 native 方法来保证原子操作,从而避免 synchronized 的高开销(重量级锁),执行效率大为提升。
2、CAS概述
CAS,即Compare And Swap的缩写,译:比较并交换,实现并发算法时常用到的一种技术。
它包含三个操作数:内存位置、预期原值及更新值。执行CAS操作的时候,将内存位置的值与预期原值比较:
- 如果相匹配,那么处理器会自动将该位置值更新为新值
- 如果不匹配,处理器不做任何操作
- 多个线程同时执行CAS操作只有一个会成功
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来一次,这种反复重试的行为就叫自旋。
t1线程从主内存读到i=5,+1后准备把6写回主内存,此时,比较期望值5和内存中的实际i值,若相等,则乐观的认为自己运算的期间没有其他线程修改i,就将i写回主内存。反之,比如主内存i=6,那t1就再来一次,i=6,i+1,期望6,此时如果主内存i=6,则写回成功,反之继续自旋。
简单说CAS就是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
3、Unsafe类
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2023) + "\t current value: " + atomicInteger.get());
//此时,已改为2023,再预期5自然修改失败
System.out.println(atomicInteger.compareAndSet(5,2023) + "\t current value: " + atomicInteger.get());
此时只有一个main线程,没有其他线程争抢,预期值正确,CAS第一次就成功:
compareAndSet方法源码:
其中U为Unsafe对象,继续往下跟是调用Unsafe对象的方法源码:
关于Unsafe类:
原子类靠的CAS思想,CAS思想则又是依靠Unsafe类中的各个native方法来落地实现的。
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存(jdk/jre/rt.jar/sun/misc/Unsafe.class)。注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用作系统底层资源执行相应任务
。
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger .getAndIncrement();
跟进方法:
do-while实现更新失败时继续自旋,var1即this,var2即内存地址偏移量,var5即期望值,var5+var4即更新值,在这里的传值就是当内存值和期望值不相等时加1,然后继续循环。
4、Unsafe汇编
new AtomicInteger(3).getAndIncrement();
上面的源码再往下到native方法:
该本地方法,实现于unsafe.app:
以Window10为例:
CAS对应在底层是CPU的一条原子指令cmpxchg,执行cmpxchg指令时,先判断操作系统是否为多核,是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后执行CAS操作,也就是说CAS的原子性是CPU实现独占的,相比synchronized,它的排他时间更短,因此性能比synchronized更优。CAS这种非阻塞的原子性操作,是通过硬件保证了比较-更新的原子性。
原语,操作系统用语,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法,调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语。
总之:
CAS是靠硬件实现的,从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性,实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。
CAS核心思想就是:比较要更新变量的值V和预期值E (compare),相等才会将V的值设为新值N (swap),如果不相等自旋再来。
5、原子引用AutomicReference
AtomicInteger是原子整型,可否有其它自定义类型的原子类型,比如AtomicBook ⇒ AutomicReference<T>
@Data
@AllArgsConstructor
class User{
String userName;
int age;
}
public class AtomicDemo2 {
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<>();
User z3 = new User("z3",22);
User li4 = new User("li4",23);
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());
}
}
期望是张三User,是则改为李四,改为李四后再期望张三,本次set失败:
6、手写自旋锁SpinLock
不用synchronized,用自旋思想完成加锁:
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t" + "----come in");
//自旋,必须当前原子引用类对象值为null,即没有其他线程占用,才抢锁成功
while(!atomicReference.compareAndSet(null,thread)){
}
}
public void unlock(){
Thread thread = Thread.currentThread();
//期望值为当前线程,改为null
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName() + "\t" + "----over,unlock...");
}
}
通过CAS操作完成自锁,A线程先进来调ImyLock 方法自已持有锁5 秒钟,B随后进来后发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
//5s释放锁
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unlock();
},"A").start();
//休眠200ms,让A线程优先于B启动
Thread.sleep(200);
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unlock();
},"B").start();
}
7、CAS的两大缺点
CAS落地后,在多线程下能性能优于synchronized的完成任务,但也有缺点:
- 循环时间长会导致开销很大
- ABA问题
ABA问题,比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。解决ABA问题的思路就是使用戳记流水。
ABA的demo:
public class ABA {
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
atomicInteger.compareAndSet(100,101);
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
//A线程再B线程比较内存值前,又改回了100
atomicInteger.compareAndSet(101,100);
},"A").start();
//休眠,保证ABA出現
Thread.sleep(300);
new Thread(() -> {
//虽然中间被A改过两次,但这里比较内存值仍能成功
System.out.println(atomicInteger.compareAndSet(100, 200) + "\t" + atomicInteger.get());
},"B").start();
}
}
8、AtomicStampedReference类解决ABA问题
public class ABA {
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号:" + stamp);
//暂停500ms,以确保让B线程拿到的初始版本号和A线程一样
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t" + "二次版本号:" + stampedReference.getStamp());
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t" + "三次版本号:" + stampedReference.getStamp());
},"A").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
//获取完时间戳后,等待2s,保证能发生ABA
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号:" + stamp);
boolean result = stampedReference.compareAndSet(100, 200, stamp, stamp + 1);
System.out.println(result + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
},"B").start();
}
}
此时B线程期望的时间戳为放开始的stamp,自然修改失败,ABA成功解决:
总之,解决ABA就直接比较+版本号一起上。