文章目录
更多相关内容可查看
CAS原理
CAS(compareAndSwap)
也叫比较交换,是一种无锁原子算法,其作用是让CPU
将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且CAS
操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)
简单总结一下 : CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
CAS源码示例分析
可以以下面这组代码作为示例,这组代码为Java中Random类利用AtomicLong中的方法来利用CAS来实现的
这里定义了一个 AtomicLong 类型的静态变量 seedUniquifier,并为其分配了一个初始值 8682522807148012L。AtomicLong 是 Java 并发包中的一个类,用于在高并发环境下提供线程安全的 long 类型的原子操作。
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
compareAndSet
是一个原子操作,它首先比较 seedUniquifier
的当前值是否等于 current
,如果是,则将其更新为 next
并返回 true
。如果其他线程已经修改了 seedUniquifier
的值,则该方法会返回 false
,并重新进入循环,尝试获取新的当前值并计算新值。
这种方法的一个关键优点是它可以确保在多线程环境下为每个线程提供一个唯一的种子值,而无需任何锁或其他同步机制。每次调用
seedUniquifier 方法时,它都会尝试基于当前种子值生成一个新的种子值,并使用 compareAndSet
方法确保该新值被成功地设置。
CAS的特点(ABA)
ABA问题
- 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
- ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
- 如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
ABA情况演示:
// 初始化为100
AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
//线程计数
CountDownLatch countDownLatch=new CountDownLatch(2);
new Thread(()->{
try {
// 等待t2获取值
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
countDownLatch.countDown();
},"t1").start();
new Thread(()->{
int oldVal=atomicReference.get();
try {
// 等待t1修改ABA
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(oldVal,102);
countDownLatch.countDown();
},"t2").start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终结果:"+atomicReference.get());
最后还是更新为102了
为了解决ABA,可以为其加上版本号,每次线程对原子对象做更新时,版本号也随之更新
通过匹配内存值与旧值以及匹配版本号是否相等两个条件来判断是否替换,当然这个版本号肯定大部分情况是需要唯一不相同的,时间戳正好满足这个特点
所以提供了AtomicStampedReference类,能够初始化对象的情况也携带版本号
通过AtomicStampedReference解决ABA:
AtomicStampedReference<String> atomicStampedReference=new AtomicStampedReference<>("A",1);
CountDownLatch countDownLatch=new CountDownLatch(2);
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println("第一次获取版本号:"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 内存值对比A,版本号对比 如果一致就更新为B,且更新版本号
atomicStampedReference.compareAndSet("A","B",atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("更新后版本号:"+atomicStampedReference.getStamp());
// B更新为A
atomicStampedReference.compareAndSet("B","A",atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("更新后版本号:"+atomicStampedReference.getStamp());
countDownLatch.countDown();
},"t1").start();
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
String oldVal=atomicStampedReference.getReference();
System.out.println("第一次获取版本号:"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 通过已经获取到的旧值和版本号做更新
atomicStampedReference.compareAndSet(oldVal,"X",stamp,stamp+1);
countDownLatch.countDown();
},"t2").start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终结果:"+atomicStampedReference.getReference());
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
Jdk中CAS
运用
整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
ATOMIC
java.util.concurrent.atomic
存放原子操作的类,这些类中提供了一些原子性操作的方法, 通过这些原子类可以避免线程安全问题的发生
虽然涉及到的类很多,但是原理和AtomicInteger都是一样,使用CAS进行的原子操作,其方法和使用都是大同小异的,这里就不做挨个分析了。
AQS
AQS简介
AQS 的全称为 AbstractQueuedSynchronizer
,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks
包下面。
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
(jdk1.7) 等等皆是基于 AQS 的。
AQS原理
- 当线程请求锁时,如果锁已被其他线程持有,则当前线程会被封装成一个Node节点并加入到等待队列的尾部。
- 当持有锁的线程释放锁时,AQS会从队列头部开始,检查节点是否处于等待状态,并唤醒该节点对应的线程来获取锁。
- 被唤醒的线程会尝试获取锁,如果成功则继续执行后续任务,否则继续进入等待队列等待被唤醒。
CLH 队列锁
- AQS内部维护了一个双向队列(FIFO),用于管理请求锁的线程。
- 当有线程请求锁时,AQS会将其封装成一个Node节点,并加入到等待队列中,线程则会进入阻塞状态。
- 当持有锁的线程释放锁时,AQS会从等待队列中唤醒一个线程来获取锁,从而实现线程的同步和互斥。
AQS 定义两种资源共享方式 :
Exclusive(独占)
只有一个线程能执行,如 ReentrantLock
。又可分为公平锁和非公平锁,ReentrantLock
同时支持两种锁,下面以 ReentrantLock
对这两种锁的定义做介绍:
- 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
Share(共享):
多个线程可同时执行,如 Semaphore/CountDownLatch
。Semaphore
、CountDownLatch
、 CyclicBarrier
、ReadWriteLock
等
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读