1.不使用synchronized的Demo
首先我们写一下代码,开两个线程为静态变量自增到2000.
public class Test1 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(()->{
try {
Thread.sleep(10);
for (int i = 0; i < 1000; i++) {
System.out.println(count++);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count = "+count);
}
}
由代码可知,此代码为非线程安全的,因此结果有时候为1999.
2.使用synchronized的Demo
接下来,我们给自增代码块引入synchronized关键字
public class Test1 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(()->{
try {
Thread.sleep(10);
for (int i = 0; i < 1000; i++) {
synchronized (Test1.class) {
System.out.println(count++);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count = "+count);
}
}
加入同步锁之后,count自增操作变成了原子操作,因此count最后等于零,但引入synchronized来实现了线程安全,并不是一个最优的选择。
关键在于性能问题。
因为synchronized关键字让没有得到锁资源的线程进入Blocked状态(挂起),而争夺到锁的线程进入Runnable状态,该过程使得操作系统在用户模式和内核模式进行频繁的切换,代价比较高。
3.使用原子操作类的Demo
因此我们引入了原子操作类。
原子操作类指的是JUC.atomic包下的一系列以Atomic开头的包装类。如AtomicBoolean,AtomicInteger,AtomicLong。他们用于对Boolean,Integer,Long类型进行的原子操作。
接下来我们尝试一下。
/**
* @author Juniors
* @date 2021/11/5 14:58
*/
public class Test2 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.incrementAndGet());
}
}).start();
}
Thread.sleep(2000);
System.out.println("count="+count.get());
}
}
因此使用AtomicInteger之后,最后的输出结果同样是200.在某些情况下,代码性能会比synchronized更好。
而Atomic操作类的底层正是用到了“CAS”机制。
4.引入CAS机制
CAS:CompareAndSwap 的缩写,即(先记录)再比较后替换。
CAS机制种使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
因此如果要更新变量的时候,只有当变量的预期值A和内存地址V中的实际值与之前记录的两值相同,才会将内存地址对应的值修改为B。
For example:
1.内存地址V当中,存储着值为10的变量。
2.此时线程1想把变量的值增加1,对线程1来说,记录的是当前的旧的预期值A=10,修改的新值B=11.
3.此时线程1将要提交的时候,被另一个线程2抢先一步,把内存地址V中的变量预期值率先更新成11.
4.此时线程1开始提交更新的时候,首先进行之前预期值A和地址V的实际值进行比较,结果发现A不等于V的实际值,因此提交失败。
5.线程1重新获取内存地址V和当前值A,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。因此重新尝试的过程被称为自旋。
5.悲观锁与乐观锁
从思想上来说,synchronized属于悲观锁,悲观的认为程序的并发情况严重,需要严防死守,而CAS属于乐观锁,乐观的认为程序中的并发情况没那么严重,因此只需让线程不断的去重新更新(自旋)就行了。
6.CAS缺点
-
CPU 开销过大
如果当前的并发量较高的话,就会有许多线程反复尝试更新某一个值,却一直不成功,循环往复,因此会给CPU带来很大压力
-
不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子操作,而不能保证整个代码块的原子性。
-
ABA问题
当A线程认为此时的预期值A为"A",但A线程更新值前的时候,已经有B,C两个线程已经进行了对预期值A的"B","A"两次更新操作,而A在进行预期值A和内存地址V进行比较时,误以为没有改变,其实变量已经改变,就出现了ABA问题。(A -》 B -》 A)
7.普通CAS的解决方案
接下来我们解决三个问题:
- Java中CAS的底层实现
- CAS操作多个变量的解决方案
- CAS的ABA问题及解决方案
7.1 Java中CAS的底层实现
下面是AtomicInteger中的常用incrementAndGet方法源码:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
private volatile int value;
public final int get() {
return value;
}
其中一段for的无限循环代码,也就是CAS的自旋,循环体中做了三件事情:
- 获取当前值
- 当前值+1,计算出目标值
- 进行CAS操作,如果成功跳出循环,否则失败继续循环自旋
我们注意到获取当前值用的是get()方法,即获取变量的当前值。
但我们如何获取当前值为内存中最新的值呢?So easy,用 volatile 关键字来保证(我们定义的变量在线程中可见性)
接下来我们看一下compareAndSet方法:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里涉及了两个重要对象,一个unsafe,一个valueOffset
其中unsafe是JVM来为我们提供间接访问底层的硬件级别的原子操作。
而valueOffset代表的是Integer对象中的value成员变量在内存中的偏移量,也就是内存地址。
7.2 CAS操作多个变量的解决方案
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
但是我们可以通过以下两种办法来解决:
1)使用互斥锁来保证原子性;
2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。
7.3 CAS的ABA问题及解决方案
接下来介绍一下我们如何解决ABA问题?
三个线程操作A,出现了A-》B-》A的状况。
该怎么解决呢?加个版本号就可以了。
真正要做到了谨慎的CAS机制,我们在compare阶段的时候不仅要将预期值A和内存地址V进行比较,而且还要比较变量的版本号是否一致。
在Java中,AtomicStampedReference类就实现了用版本号来做严谨CAS机制。
总结:
1. java语言CAS底层如何实现?
利用unsafe提供的原子性操作方法。
2.什么事ABA问题?怎么解决?
当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
利用版本号比较可以有效解决ABA问题。