Java并发基础(8)—— 原子操作CAS

目录

一、背景

二、解决方法

三、原子操作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、如果只是一个计数器,使用锁机制比较笨重

三、原子操作CAS

原子操作定义:假定有两个任务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的缺点

4.1 ABA问题

从上面介绍,CAS操作经过几个步骤,获取值,比较,修改

如果在获取值和比较之间,该值从原有的A,变为B,再变为A,CAS是不知道该值发生了变化

所以使用了版本号来解决该问题,每次变量更新都会把版本号加1,此时A→B→A就会变成1A→2B→3A

4.2 循环开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

4.3 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环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

六、demo

以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());
    }
}

---------------------------------------------------------------------------------------------------------------------------------------------------

如果我的文章对您有点帮助,麻烦点个赞,您的鼓励将是我继续写作的动力

如果哪里有不正确的地方,欢迎指正

如果哪里表述不清,欢迎留言讨论

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章