Java基础学习总结:JUC之(二)原子变量和CAS算法

一、原子变量

1、i++的原子性

 什么是原子性:

简单的可以理解为:操作是不可再分割的,比如:

int i=0;

但是 i++ 却是可以再分的。

i++的操作实际上分为三个步骤: "读-改-写",i++可拆分为:

int temp1=i;
int temp2=temp+1;
i=temp2;

 测试:

代码:

package basis.stuJUC.stuAtomic;

public class test_I {
    public static void main(String[] args) {
        int i = 1;
        i++;
        System.out.println(i);
    }
}

反编译之后的字节码: 

 Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: iload_1
         9: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        12: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 5
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I

2、i++在多线程环境下的问题

示例:

package basis.stuJUC.stuAtomic;

public class Test_I {
    public static void main(String[] args) {

        AtomicThread atomicTest=new AtomicThread();
        for (int i=1;i<=20;i++){
            new Thread(atomicTest).start();
        }
    }
}

class AtomicThread implements Runnable {

    private  int num=1;
    @Override
    public void run() {
        try {
            Thread.sleep(300);
            System.out.println(getNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public int getNum(){
        return num++;
    }
}

结果:

1
15
14
13
...
3
2
1
17
16

 有结果可见,出现了两个1,这就是由于i++的非原子性引起。

这种问题当然可以通过加synchronized 关键字来解决,那有没有另外一种方案呢,答案肯定是有的,且在jdk1.5的时候就有了,那就是Atomic包下的原子类。

3、原子类

(1)原子类

JUC包下的atomic包中的原子类,利用了现代处理器的特性,可以用非阻塞的方式完成原子操作。 
主要有: 

  • AtomicBoolean 
  • AtomicInteger 
  • AtomicIntegerArray 
  • AtomicIntegerFieldUpdater 
  • AtomicLong 
  • AtomicLongArray 
  • AtomicLongFieldUpdater 
  • AtomicMarkableReference 
  • AtomicReference 
  • AtomicReferenceArray 
  • AtomicReferenceFieldUpdater 
  • AtomicStampedReference 
  • DoubleAccumulator 
  • DoubleAdder 
  • LongAccumulator 
  • LongAdder

(2)我们使用AtomicInteger类解决i++在多线程环境中的问题,

代码:

package basis.stuJUC.stuAtomic;

import java.util.concurrent.atomic.AtomicInteger;

public class Test_I {
    public static void main(String[] args) {

        AtomicThread atomicTest=new AtomicThread();
        for (int i=1;i<=20;i++){
            new Thread(atomicTest).start();
        }
    }
}

class AtomicThread implements Runnable {

    private AtomicInteger atomicInteger=new AtomicInteger(1);
    @Override
    public void run() {
        try {
            Thread.sleep(300);
            System.out.println(getNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public int getNum(){
        return atomicInteger.getAndIncrement();
    }
}

 (3)AtomicInteger源码

package java.util.concurrent.atomic;

/**
 * @since 1.5
 * @author Doug Lea
*/
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    private volatile int value;
}

 可以看出,AtomicInteger构造函数中的值使用 volatile 关键字修饰,保证其内存可见性。

而AtomicInteger中的 “++“ 操作即 getAnIncrement() 方法调用的是Unsafe 类对象的getAdnAddInt()方法。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

 而Unsafe 类的getAdnAddInt()采用的是CAS算法即compareAndSwapInt()方法来保证 “++” 操作的原子性。

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

二、CAS算法

1、CAS算法简介

CAS(Compare-And-Swap,比较交换算法) 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;

CAS 是一种无锁的非阻塞算法(属于乐观锁)的实现;

CAS 包含了三个操作数:

  1. 进行比较的旧预估值: A
  2. 需要读写的内存值:V
  3. 将写入的更新值:B

当且仅当 A == V 时, V = B, 否则,将不做任何操作,并且这个比较交换过程属于原子操作;

//原子操作,由硬件实现
if(A==V){
    V=B;
}

使用synchronized 模式CAS算法

package basis.stuJUC.stuAtomic;
public class SimpleCAS {
    private int num=1;
    public synchronized int getNum(){
        return num;
    }
    public synchronized boolean compareAndSet(int old,int newValue){
        if(old==num){
            num=newValue;
            return true;
        }
        return false;
    }
}

 测试类:

package basis.stuJUC.stuAtomic;

import java.util.Random;

public class Demo2 {
    public static void main(String[] args) {
        SimpleCAS cas=new SimpleCAS();
        for(int i=0;i<50;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true) {
                        int old = cas.getNum();
                        boolean b = cas.compareAndSet(old, new Random().nextInt(50));
                        System.out.println(b);
                        if(b){
                            break;
                        }
                    }
                }
            }).start();
        }

    }
}

结果:

true
false
true
false
true
true
true
true

由结果可以看出,如果线程a获取旧值后,其他线程获得执行权,修改了num值,这就导致线程a拿到的旧值(A)和内存值(V)不相等,所以修改失败,返回false。

2、悲观锁和乐观锁

参考:https://blog.csdn.net/youyou1543724847/article/details/52735510

(1)悲观锁:

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

一个典型的依赖数据库的悲观锁调用:

select * from account where name=”Erica” for update

这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。 
本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

(2)乐观锁

乐观锁(Optimistic Locking)相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。

乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。

读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

3、CAS算法的ABA问题

(1)什么是ABA问题

 在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。
假设如下事件序列:

  1. 线程 1 从内存位置V中取出A。
  2. 线程 2 从位置V中取出A。
  3. 线程 2 进行了一些操作,将B写入位置V。
  4. 线程 2 将A再次写入位置V。
  5. 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。

(2)测试

我们再对上述模拟的CAS算法进行测试,在对num的值修改之后,再改回原值(即ABA):

修改模拟的CAS算法:

public synchronized boolean compareAndSet(int old,int newValue){
        if(old==num){
            num=newValue;
            num = old;
            return true;
        }
        return false;
    }

测试类不变,运行程序发现,每个线程的操作都能成功。即使是线程a在获取旧值以后,num的值被其他线程修改了,但是由于修改之后还是原值,此时线程a依旧认为num没有被修改过,依旧能执行成功。

解决ABA问题的方法就是对数据添加一个标识,该标识称为数据的版本号,数据每被修改一次,版本就会变动一次,可以通过对比数据的版本号来判断该数据是否被修改过。JUC的atomic包中的原子标记参考 AtomicStampedReference 类 提供了这种功能,所以说CAS算法是乐观锁的一种实现。

4、AtomicStampedReference(原子标记参考)的使用

package basis.stuJUC.stuAtomic;

import java.util.Random;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Demo3 {
    public static void main(String[] args) {

        AtomicStampedReference<Integer> asr=new AtomicStampedReference<>(1, 1);
        for(int i=0;i<20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //获取旧值
                    Integer old=asr.getReference();
                    //获取旧版本号
                    int version = asr.getStamp();
                    //如果旧值和版本号均与内存中的数据保持一致,那么认为还是原数据,可以执行操作。然后令版本号加一。
                    //如果旧值和版本号其中一个与内存中的数据不一致,就认为该数据在读取旧值之后又被其他线程修改过了,
                    // 需要重新取值,重新判断。
                    boolean b=asr.compareAndSet(old, new Random().nextInt(1000), version, version+1);
                    System.out.println(b);
                }
            }).start();
        }
    }
}

 AtomicStampedReference的构造方法接受两个值,一个是被版本号标记的数据对象,一个是版本号的初始值。

AtomicStampedReference 的 compareAndSet()方法用于比较旧值和内存值、旧版本号和新版本号,compareAndSet()方法源码:

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

 compareAndSet()中使用casPair()方法比较两对值是否一致,而在casPair()方法中使用的是CAS算法。

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

5、总结

CAS算法保证对数据操作的原子性,解决的是非原子操作在读取值和设置值的期间,操作对象的值被其他线程修改的问题。但是会出现ABA问题,即对内存中的数据是否被修改过,不是非常的敏感。而AtomicStampedReference类对数据提供一个版本号,数据每进行一次修改,版本号就改变一次。AtomicStampedReference底层使用的也是CAS算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值