项目地址
JUC_04
CAS
CAS是C++写的,底层已经实现了原子性,所以需要使用UnSafe类,UnSafe类中使用jni技术来调用CAS中的compareAndSet,所以我们在 java只能看到compareAndSwapObject、compareAndSwapInt、compareAndSwapLong这三个方法是有四个参数,而第一个就是需要改变的对象(也就是this),后面两个可以分为E,N(前两个合并就是V)。当且仅当V=E时,将V=N
V:内存值(共享变量)
E:旧预值(工作内存)
N:新值(修改后的值)
在前面提到的compareAndSwapObject、compareAndSwapInt、compareAndSwapLong三个方法,为什么他们都是有四个参数呢?其实第一个参数为本对象,第二个就是变量的存储地址偏移量,只有拥有前面两个参数才能准确找到某个的某个共享变量。
执行流程
- 获取内存值V,赋值给E
- 如果要修改V的值
如果V == E,说明共享变量V没有发生变化,就直接将V的值改为N
如果V != E,说明有其他线程将V的值进行了更改,则在从1开始
在第二步的重新读取的过程称为自旋。
使用AtomicLong写出案例:
import sun.misc.Unsafe;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author 龙小虬
* @date 2021/4/23 10:43
*/
public class Main extends Thread {
private static AtomicLong atomicLong = new AtomicLong();
@Override
public void run() {
while (atomicLong.get()<1000){
System.out.println(Thread.currentThread().getName() + "," + atomicLong.incrementAndGet());
}
}
public static void main(String[] args) throws InterruptedException {
Long startTime=System.currentTimeMillis();
Main main1 = new Main();
Main main2 = new Main();
main1.start();
main2.start();
main1.join();
main2.join();
Long endTime=System.currentTimeMillis();
System.out.println(endTime-startTime);
}
}
1.atomicLong.get()
获取value值
2.atomicLong.incrementAndGet()
value做自增
我们来看看这个自增的方法。
我们先看getAndAddLong()方法。
1.this.getLongVolatile(var1, var2);
获取工作内存的value值,也就是主内存中获取value
2.this.compareAndSwapLong(var1, var2, var6, var6 + var4)
利用compareAndSet(),对主内存数据进行修改,并对比工作空间和主内存变量中的值是否相同,如果不相同则继续执行步骤1,直至相同,最后更改主内存数据为原主内存数据+1。
在getAndAddLong()方法中,为什么他的调用之后,返回的是原主内存数据中的数据,为什么不是返回最新的主内存数据,而是在原主内存数据+1,猜测是因为do…while(),如果再去查找一次那么则会降低效率,一旦是几百个线程去修改这个数据,那么相当于有99个线程都需要再多一次自旋,而且在修改之前,直接从主内存获取数据,所以没有必要返回值的时候再去查找。
基于AtomicLong手写锁
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
/**
* @author 龙小虬
* @date 2021/4/23 13:32
*/
public class AtomicTryLock {
//定义AtomicInteger
//0表示该锁已经被使用
//1表示未被使用
private volatile AtomicInteger atomicInteger = new AtomicInteger(0);
// 存放当前获取到锁的线程名
private Thread lockCurrentThread;
//尝试获取锁
public boolean tryLock() {
boolean result;
// 加入自旋
while (!(result = atomicInteger.compareAndSet(0, 1))) {
System.out.println(Thread.currentThread() + ",自旋中.....");
}
lockCurrentThread = Thread.currentThread();
System.out.println(lockCurrentThread + ",获取锁成功");
return result;
}
//释放锁
public boolean unLock() {
// 只有获取到锁的线程才能释放锁
if (lockCurrentThread != null && lockCurrentThread != Thread.currentThread()) {
return false;
}
return atomicInteger.compareAndSet(1, 0);
}
public static void main(String[] args) {
AtomicTryLock atomicTryLock = new AtomicTryLock();
IntStream.range(1, 10).forEach((i) -> new Thread(() -> {
// 不管发生什么,线程结束都需要释放锁
try {
atomicTryLock.tryLock();
} catch (Exception e) {
e.printStackTrace();
atomicTryLock.unLock();
} finally {
atomicTryLock.unLock();
}
}).start());
}
}
还有个问题,在高并发下,我觉得CAS的ABA问题会很严重。
我们来谈谈ABA问题吧。
CAS的ABA问题
如果将原来的值A,改为了B,B有改为了A 发现没有发生变化,实际上已经发生了变化,所以存在ABA问题。
那么经典案例是什么?为什么要去解决这个问题?
CAS经典案例:
用户要给储值卡充值20元,用户卡内余额15元,A线程首先获取余额是15,然后准备加上20,A线程因为某种原因暂停了,而B线程成功将余额加上20并且成功提交,此时余额为35。但是紧接着用户又消费了20,所以余额还是15,终于A线程获取到了时间片,它比对之后发现余额还是15,所以A线程就执行了。
ABA的问题核心在于一个线程在提交的时候,如果只是根据要修改的值和之前是否一样,这样是无法证明这个值没有被其他线程改过。因为在这段时间它的值可能被改为其他值,然后又改原来的,实际上如果能避免重复提交就能避免ABA问题,而版本号控制可以避免重复提交 -----> 这也就是解决ABA的方案
官方也给出了AtomicMarkableReference对象来解决这个问题。
使用方法:
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author 龙小虬
* Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,那么即使两个对象的值相同,CAS方法都会返回false
* @date 2021/4/23 13:49
*/
public class AtomicStampedReferenceTest {
// 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false
private static final Integer INIT_NUM = 100;
private static final Integer UPDATE_NUM = 200;
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);
public static void main(String[] args) {
new Thread(() -> {
Integer value = (Integer) atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第一个参数expectedReference:表示预期值
//第二个参数newReference:表示要更新的值
//第三个参数expectedStamp:表示预期的时间戳
//第四个参数newStamp:表示要更新的时间戳。
if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
} else {
System.out.println("版本号不同,更新失败!");
}
}).start();
}
}
这里面就行了版本号的对比。