Java中CAS与AQS
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 机器学习入门之pandas的使用
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
Java中CAS与AQS以及预备的锁知识
提示:以下是本篇文章正文内容,下面案例可供参考
一、并发控制
当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。
没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
二、悲观锁(Pessimistic Lock)
1.理解
代码如下(示例):
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。
悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:
- 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
- Java 里面的同步 synchronized 关键字的实现。
2.悲观锁主要分为共享锁和排他锁:
- 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
三、乐观锁(Optimistic Locking)
理解
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:
CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
说明
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
所得实现详见mysql部分
四、CAS
1、定义
cas是一个无锁解决方案,更准确的是采用乐观锁技术,实现线程安全的问题
CAS(Compare-And-Swap),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。CAS是一种系统原语,Java中利用原子操作类实现,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
package CAS;
import java.util.concurrent.atomic.AtomicInteger;
public class threadeg2 {
public static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<2;i++) {
new Thread(
new Runnable() {
public void run() {
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
for(int j=0;j<20;j++) {//每个线程对count自增20
count.incrementAndGet();
}
}
}).start();
}
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
使用AtomicInteger之后,最终的输出结果可以保证是20。有人说可以用synchronized关键字来保证线程安全。但是使用AtomicInterger类在某种程度上会比synchronized关键字好,因为synchronized关键字对于没有竞争到锁资源的线程会进行阻塞处理,这时候发生用户态与内核态之间的转换,十分消耗资源,代价较大。尽管后面的对synchronized进行了优化,但是线程一多还是存在该问题。而Atomic操作类的底层正是用到了“CAS机制”。
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
1、变量内存地址,V表示
2、旧的预期值,A表示
3、准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)
2、CAS的三大缺点
CAS的缺点主要有3点:
(1)ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
(2)循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。(不知道自旋是什么的可以看我写的关于锁的文章。)
(3)只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
解决 CAS 恶性空自旋的较为常见的方案为:
分散操作热点,使用 LongAdder 替代基础原子类 AtomicLong。
使用队列削峰,将发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度。JUC 中非常重要的基础类 AQS(抽象队列同步器)就是这么做的。详见
https://blog.youkuaiyun.com/lwang_IT/article/details/121638089
ABA问题空讲有点不明白所以我们举个例子。
假设,小童银行卡里有1000元。要用一个遵循CAS的提款机提款500元去买衣服。此时由于提款机硬件开小差,将小童的提款操作提交了两次,此时有两个线程,两个线程都是获取当前值1000元,期望更新值为500元。正常情况下,应该是两个线程一个成功一个失败,小童的余额被扣款一次。
此时的状态为:
此时线程2仍在阻塞,所以提款机执行线程3。且由于CAS检测成功,所以线程3执行成功。
这时候线程2恢复运行,由于一开始线程2获取的当前值是1000,且现在的值也为1000,所以CAS检测成功,将值更改为500。
原本线程2应当提交失败,小童的银行卡余额应该保持为1000元,结果由于CAS里的ABA问题导致线程2提交成功了。所以小童很悲催的少掉了500块钱。
既然遇到了这个问题那么怎么解决ABA问题呢?
ABA问题java中的解决方式
原子引用:(存在ABA问题)
package InterviewTest;
import java.util.concurrent.atomic.AtomicReference;
class User{
String name;
int age;
public User(String name,int age) {
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3",25);
User li4 = new User("li4",25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference);
System.out.println(atomicReference.compareAndSet(z3, li4)+
" "+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(li4, z3)+
" "+atomicReference.get().toString());
}
}
带版本号的原子引用(解决ABA问题)
AtomicStampedReference版本号原子引用(或者AtomicMarkableReference):
基本和AtomicStampedReference差不多,AtomicStampedReference主要关注版本号,即reference的值被修改了多少次;AtomicMarkableReference是使用boolean mark来标记reference是否被修改过
既然有了 AtomicStampedReference 为啥还需要再提供 AtomicMarkableReference 呢,在现实业务场景中,不关心引用变量被修改了几次,只是单纯的关心是否更改过。
案例:两种原子引用的对比
package InterviewTest;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
static AtomicReference<Integer> atomicReference
= new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference
= new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("************以下是ABA问题的产生**************");
new Thread(()->{
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019)
+" "+atomicReference.get());
},"t2").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("************以下是ABA问题的解决**************");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()
+" "+" 第一次版本号:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,
101,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+" "+" 第2次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,
100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+" "+" 第3次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()
+" "+" 第一次版本号:"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(
100,
2019,
stamp,
stamp+1);
System.out.println(Thread.currentThread().getName()+
" 修改成功否:"+result+" 当前最新实际版本号:"
+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+
" 当前实际最新值:"
+atomicStampedReference.getReference());
},"t4").start();
}
}
************以下是ABA问题的产生**************
true 2019
************以下是ABA问题的解决**************
t3 第一次版本号:1
t4 第一次版本号:1
t3 第2次版本号:2
t3 第3次版本号:3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值:100
更详细的可以阅读
https://blog.youkuaiyun.com/lwang_IT/article/details/121638089
的文章
————————————————
此篇文章为学习笔记,是对别人知识的理解,加上自己的一些个人理解汇聚而成。若有侵权联系删除。
原文链接:https://blog.youkuaiyun.com/qq_51720181/article/details/125618007
https://blog.youkuaiyun.com/weixin_48321825/article/details/121094091