🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
1. CAS问题
1.1 什么是CAS
CAS全程compare and swap,字面意思就是比较和交换(与其说是交换,不如说是赋值),在底层会涉及到一下操作.
底层中涉及到的对象有内存和两个寄存器.
- 首先比较内存地址中的值和寄存器1的值是否相等.
- 如果相等,把寄存器2中的值赋给内存中.
- 如果不相等,不进行任何操作
- 返回值为是否赋值成功.
注意:上述的操作为原子操作.
1.2 伪代码
注意:下面的代码不是原子的,只是用于辅助理解CAS的过程,实际的CAS问题在硬件底层中是原子的.
boolean CAS(address,expectValue,swapValue){//address内存中的值,expectValue寄存器1,swapValue寄存器2
if(address == expectValue){
address = swapValue;
return true;
}
return false;
}
1.3 CAS是如何实现的
简而言之,是因为硬件方面提供了支持,软件层面才可以做到.由CPU提供了上述指令,因此操作系统内核也能够完成这样的操作,之后OS会提供出响应的api,JVM对OS提供出的api进行封装,我们便可以在Java中使用CAS.
1.4 CAS的有哪些应用
1.4.1 实现原子类
标准库中提供了java.util.concurrent.atomic
包,里面的类都是基于这种方式来实现的.
典型的就是AtomicInteger类.其中的getAndIncrement相当于i++操作.
- 该类的构造方法可以指定一开始变量的初始值.
- increamentAndGet --> ++i
- getAndIncrement–> i++
- decreamentAndGet --> --i
- getAndDecreament --> i–
- getAndAdd(10) --> i+=10
import java.util.concurrent.atomic.AtomicInteger;
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(0);
Thread thread = new Thread(()->{
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
}
});
Thread thread1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(atomicInteger);
}
}
运行结果:
伪代码实现
class AtomicIntegter{
private int value;
public int getAndIncrement(){
int oldValue = value;
while (!CAS(value,oldValue,oldValue+1)){
oldValue = value;
}
return oldValue;
}
}
总地来说,原子类没有用到任何的加锁操作(是因为CAS没有用到加锁),使得代码的效率更高,但是这种共操作只适用于部分场景,加锁的使用场景还是比原子类更加通用.
1.4.2 实现自旋锁
伪代码实现:
public class SpinLock{
private Thread owner = null;
public void lock(){
while(!CAS(owner,null,Thread.currentThread()){//判断锁是否被占用,没有就使用当前线程赋值
}
}
public void unlock(){
this.owner = null;//解锁之后赋值为null
}
}
1.5 CAS的ABA问题
1.5.1 什么是ABA问题
现在存在两个线程,t1线程和t2线程,t1线程要想进行CAS,需要进行一下操作:
- 先读取num的值,记录到oldNum变量中.
- 使用CAS判定当前num的值是否为A,如果为A,就修改成Z.
如果有一个线程t2在这个中间对当前num的值进行了修改.只不过就是从A改成了B又一次改回了A.
举例说明:翻新机
这就好比你买来一个新手机,你无法判断这是一个全新的一手手机,还是有一些无良商家对二手机进行了翻新再卖给你.
1.5.2 ABA问题带来的bug
大部分情况下,ABA问题不会造成什么bug,但是不排除出现一些特殊情况会出现bug.
举例说明:钟离去银行取钱
有请助教:钟离,达达利亚
- 正常的过程:
t1线程希望从总结金额100中扣款50,这时候t2线程也希望如此,假设t1先进行了CAS操作,t2阻塞等待,t1线程的CAS操作把new值赋值为old-50之后,new值就变为50.之后在t2进行CAS操作的时候,t2就发现new值和old值不一样,就不会进行CAS操作.
- 异常的情况
在钟离取款的时候,由于达达利亚考虑到钟离每次都可能不带钱,所以在钟离取款的时候,达达利亚又给钟离打了50块钱.在t1线程执行完CAS操作之后,t2还没有执行CAS,这时候进行了打款操作,t2进行CAS操作的时候,认为old值和new值相等,所以t2也进行了一次扣款操作.这时候就产生了bug.
1.5.3 解决方案
要给修改的值引入版本号.版本号只加不减,每次操作一次余额之后,版本号+=1.在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
- 在CAS操作在读取旧值的时候,也要读取版本号.
- 在真正修改的时候:
- 如果当前版本号和读到的版本号一样的时候,修改数据.
- 如果当前版本号高于读到的版本号的时候,就操作失败了.
继续拿前面的钟离取款的例子来说明:
在t1线程进行CAS的时候,进行了扣款操作,版本号+1,之后t3线程进行打款,对版本号+1,之后在t2线程CAS的时候,发现当前版本号高于之前读取到的版本号,则操作失败.
2. JUC包
JUC包,全称java.util.concurrent
.其中存放了和多线程相关的组件.
2.1 Callable接口
该接口和Runnable相同,是描述一个任务的,只不过call方法有返回值,而run方法没有返回值.就是相当于给线程封装了一个返回值.
代码实例:计算1+2+3+…+1000的值
- 不使用Callable的接口
public class Demo28 {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
int result = 0;
for (int i = 0; i < 1000; i++) {
result += i;
}
sum = result;
});
thread.start();
thread.join();
System.out.println(sum);
}
}
- 使用Callable接口
- 创建一个匿名类,实现Callable接口的call方法,方法中是累加过程,泛型参数表示返回值的类型.
- 把callable实例用FutureTask来包装一下,引用设置为futureTask.
- 创建线程,构造方法传入futureTask,启动线程.
- 之后通过futureTask的get方法去获取到call方法的返回值.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo27 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int ret = 0;
for (int i = 0; i < 5000; i++) {
ret++;
}
return ret;
}
};//通过匿名内部类的方法重写call方法
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//通过FutureTask来包装callable
Thread thread = new Thread(futureTask);
thread.start();//线程开始执行任务
thread.join();
System.out.println(futureTask.get());//通过futureTask的get方法;来拿到call的返回值
}
}
运行结果:
举例说明:
有请助教:香菱,莱欧斯利
今天,莱欧斯利去万名堂吃饭,万名堂的前台招待员为莱欧斯利点好餐之后,就会产生一个菜单(call方法中的一堆东西),之后会生成小票(futureTask),小票一份值后厨联,一份是顾客联,之后后厨联的小票就会传到香菱的手中(把futureTask传给线程),香菱就会在后厨一通输出,在香菱做好之后,莱欧斯利就可以通过顾客联取到餐(get方法).