1 cas What?
cas是compareandswap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
2 引出 CAS
需求:
开发一个网站,对访问量进行统计,用户每发送一个请求,访问量+1,如何实现?
模拟100个用户同时访问,并且每个人对咱们的网站发起10次请求,最后的总访问次数100*10=1000
2.1 示例1(线程不安全,结果不正确)
代码
public class CAS01 {
// 总访问量
static int count = 0;
// 模拟访问的方法
public static void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
count++;
}
public static void main(String[] args) throws InterruptedException {
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
// 开始时间
Long startTime = System.currentTimeMillis();
// 模拟100个用户
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
// 每个线程就是一个用户
// 模拟每个用户发起十次访问请求
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每个线程结束之后 threadSize -1
countDownLatch.countDown();
}
});
thread.start();
}
// 保证100个线程结束之后,再执行后面的代码
countDownLatch.await(); // threadSize==0 执行后面的代码
Long endTime = System.currentTimeMillis();
System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
}
}
结果
分析
Q: 为什么count!=1000,问题出在哪里了?
A: count++ 操作实际上是由3步来完成
1.获取 count 的值,记做 A : A = count;
2.将 A 的值 +1,得到 B:B=A+1
3.将B的值赋值给 count
如果有两个线程同时执行 count++,两个线程执行到上面步骤第一步,得到的count 的值是一样的,3步操作结束后,count 只有+1,导致count结果不正确!
Q: 怎么解决结果不正确问题?
A: 对 count++ 操作的时候,我们让多个线程进行排队,多个线程同时到达 request() 方法的时候
只能允许一个线程可以进行操作,这样操作的 count++ 就是排队进行的,结果一定是正确的
Q: 怎么实现排队效果?
A: Java 中 synchronized 关键字和 ReentrantLock 都可以实现对资源加锁,保证并发正确性。
2.2 示例2 synchronized 保证线程安全
代码
public class CAS02 {
// 总访问量
static int count = 0;
// 模拟访问的方法
// synchronized 修饰static request()方法,修饰的是类对象,锁的是类
public static synchronized void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
count++;
}
public static void main(String[] args) throws InterruptedException {
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
// 开始时间
Long startTime = System.currentTimeMillis();
// 模拟100个用户
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
// 每个线程就是一个用户
// 模拟每个用户发起十次访问请求
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每个线程结束之后 threadSize -1
countDownLatch.countDown();
}
});
thread.start();
}
// 保证100个线程结束之后,再执行后面的代码
countDownLatch.await(); // threadSize==0 执行后面的代码
Long endTime = System.currentTimeMillis();
System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
}
}
结果
分析
发现用 synchronized
关键字虽然完成了线程同步,成功输出 1000,但是发现十分耗时,说明 synchronized
关键字十分浪费资源,很大程度上锁住了一些没有不要锁的地方。
Q: 耗时太长的原因是什么?
A: 程序中 的request方法使用 ``synchronized 关键字修饰,保证了并发情况下的线程安全问题。但是这样子的加锁使得相当于串行执行了。
Q: 如何解决耗时长的问题?
A: count++ 操作实际上是由3步来完成
1.获取 count 的值,记做 A : A = count;
2.将 A 的值 +1,得到 B:B=A+1
3.将B的值赋值给 count
升级第三步的实现
- 获取锁
- 获取以下 count 最新的值,记做 LV
- 判断LV 是否等于 A ,如果相等,则将 B的值赋值给 count ,并返回 true 否则 返回 false
- 释放锁
这样就会使得锁的范围就是第三步,所系了范围。
2.3 示例3 模拟CAS 保证线程安全
代码
public class CAS03 {
// 总访问量
volatile static int count = 0;
// 模拟访问的方法
public static void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
int expectCount; // 表示期望值
while (!compareAndSwap(expectCount = getCount(), expectCount + 1)) {
}
}
public static void main(String[] args) throws InterruptedException {
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
// 开始时间
Long startTime = System.currentTimeMillis();
// 模拟100个用户
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
// 每个线程就是一个用户
// 模拟每个用户发起十次访问请求
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 每个线程结束之后 threadSize -1
countDownLatch.countDown();
}
});
thread.start();
}
// 保证100个线程结束之后,再执行后面的代码
countDownLatch.await(); // threadSize==0 执行后面的代码
Long endTime = System.currentTimeMillis();
System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
}
/**
* @param expectCount 期望值 count
* @param newCount 需要给 count 赋值的新值
* @return 成功返回 true 失败返回 false
*/
public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
if (getCount() == expectCount) {
count = newCount;
return true;
}
return false;
}
private static int getCount() {
return count;
}
}
结果
发现耗时时间变短了,而且又保证了线程安全
java中也有cas的应用
java从jdk1.5就将cas引入并且使用了,java中的Atomic系列就是使用cas实现的
例如,AtomicInteger(package java.util.concurrent.atomic;
),有兴趣的了解的 点击我
3 CAS 能做什么?
可以解决多线程并发安全的问题,以前我们对一些多线程操作的代码都是使用synchronize关键字,来保证线程安全的问题;
现在我们将cas放入到多线程环境里我们看一下它是怎么解决的
我们假设有A、B两个线程同时执行一个int值value自增的代码,并且同时获取了当前的value,我们还要假设线程B比A快了那么0.00000001s,所以B先执行,线程B执行了cas操作之后,发现当前值和预期值相符,就执行了自增操作,此时这个value = value + 1;
然后A开始执行,A也执行了cas操作,但是此时value的值和它当时取到的值已经不一样了,所以此次操作失败,重新取值然后比较成功,然后将value值更新,这样两个线程进入,value值自增了两次,符合我们的预期。
4 CAS的缺点
1.CPU开销较大,长时间自旋非常消耗资源
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。。