目录
一、背景
二、解决方法
三、原子操作CAS
四、CAS的缺点
4.1 ABA问题
4.2 循环开销大
4.3 只能保证一个共享变量的原子操作
五、原子操作类
六、demo
我们都知道在多线程环境下,num++,这个操作是不安全的,因为它不是原子操作
在底层,这个加1的操作会被分成几个步骤:
1、从内存中读取 num
2、然后执行 num + 1
3、然后把新值写入内存
直接看代码
public class TestInt {
private volatile int num = 0;
public int getNum() {
return this.num;
}
public void increase() {
this.num++;
}
public static void main(String[] args) {
final TestInt testInt = new TestInt();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
testInt.increase();
}
}).start();
}
// 让所有子线程执行完毕
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num:" + testInt.getNum());
}
}
按照我们想的,num结果应该为100,实际测试时,结果却每次都不一样(小于或者等于100)
比如说有下面这种情况:
线程A获取到num的值 = 0,由于它不是原子性,cpu资源被线程B抢走(或者A的时间片执行时间已到)
线程B获取num的值 = 0
线程B对num + 1 = 1
线程B把num值更新到主内存中后结束
线程A重新获得cpu资源, 对它内存中num的副本 + 1 = 1
线程A将num = 1更新到主内存中
本来应该num应该为2的,结果却为1
为了保证线程的安全,我们第一步就想到使用synchronized关键字加锁,这样肯定可以解决问题
但是锁机制也不是在任何情况下都是最优选择
synchronized是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁
这样可能会造成以下问题:
1、被阻塞的线程优先级很高
2、获得锁的线程一直不释放锁
3、大量的线程来竞争锁,导致CPU资源的浪费
4、如果只是一个计数器,使用锁机制比较笨重
原子操作定义:假定有两个任务A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的
那么CAS是如何做到原子操作的呢?
它是利用了现代处理器都支持的CAS指令,循环这个指令,直到成功为止
也就是说,它不是通过语言级别的操作来保证原子操作,而是在更底层,CPU指令级别的操作保证了原子操作
CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B
如果这个地址V上存放的值(也就是内存中的值)等于这个期望的值A,则将地址上的值赋为新值B
上述动作是在一个循环中进行(for(;;){},也称为自旋操作,其实就是一个死循环),直到修改成功
我们可以先看一下CAS实现类AtomicInteger中如何实现类似++i的源码
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
举个例子
A,B两个线程同时修改变量num的值0,进行加1操作
1、获取值。首先他们都会去内存中获取num=0,拷贝到自己工作空间,此时他们的期望值都是0
2、计算新值
3、比较和交换。内存中的值与期望值相等,则交换。这里通过CAS指令,保证了比较和交换是一个原子操作
并且要注意:该值是volatile修饰,保证了值修改时,其他线程可以立马知道
A和B都执行完第2步,第3步只有一个线程可以成功执行
这里为了好理解,假设A快一点,先执行第3步,先比较期望值0和内存中值是否相等,发现相等,把新值1刷回内存中,然后返回结束循环
B慢一点,比较期望值0和内存中值(此时已变为1,因为是volatile修饰)是否相等,不相等,则执行下一次循环
再获取内存中的值1拷贝到自己工作空间,也就是期望值为1
再计算新值为2
再比较和交换,这时相等,就把新值刷回内存,然后返回结束循环
从上面介绍,CAS操作经过几个步骤,获取值,比较,修改
如果在获取值和比较之间,该值从原有的A,变为B,再变为A,CAS是不知道该值发生了变化
所以使用了版本号来解决该问题,每次变量更新都会把版本号加1,此时A→B→A就会变成1A→2B→3A
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就需要用锁机制
但是,我们可以把多个共享变量合并成一个变量,来进行CAS操作
java在java.util.concurrent.atomic包下,为我们提供了一系列以Atomic开头的包装类,方便我们使用
jdk1.5的atomic包下提供的原子操作类
标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
复合变量类:AtomicMarkableReference,AtomicStampedReference
jdk1.8之后又添加了下面的四个类
LongAdder DoubleAdder 高并发情况下替代AtomicLong
LongAccumulator DoubleAccumulator
以AtomicInteger为例,提供了getAndIncrement()(类似i++操作)、incrementAndGet()(类似++i操作)、get()等方法
public class TestAtomicInt {
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
System.out.println(num.getAndIncrement());
System.out.println(num.incrementAndGet());
System.out.println(num.get());
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------
如果我的文章对您有点帮助,麻烦点个赞,您的鼓励将是我继续写作的动力
如果哪里有不正确的地方,欢迎指正
如果哪里表述不清,欢迎留言讨论