欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java并发编程---原子操作CAS

程序员文章站 2022-03-08 20:10:27
...

实现并发操作的方法有两种:一种是使用锁(Synchronized和Lock),另外一种是使用原子操作(CAS)

Synchronized基于阻塞的锁机制可能会带来的问题:

a. 被阻塞的线程优先级很高
b.拿到锁的线程一直不释放锁怎么办?
c.大量的竞争消耗cpu,同时带来死锁或者其他安全问题

基于上述问题,提出了CAS原子操作
a.CAS原理:利用现代处理器都支持的CAS指令,循环这个指令,直到成功为止
b.CAS(Compare And Swap) : 指令级别上保证这是一个原子操作
c.与CAS相关的三个运算符:一个内存地址V,一个期望值A,一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋新值B,如果不是,不做任何操作。
(在(死循环)循环中不断进行CAS操作)

d.CAS也会带来一定的问题:

1)ABA问题(A–>B–>A):当第一次取内存地址V时,取到了A的值,可当它要执行CAS操作之前,已经有一个线程对A的值改为B后,再 改为A,这时候的值虽然一样,不过内容已经有过更改。(举例:在现实生活中,你放在座子上的以一杯水被同事喝完,又重新倒满一杯放在那里。很明显,它虽然还是那杯满着的水,可性质上却完全不一样了。) 解决方法:可以通过版本号解决 A1—>B2---->A3 (类似于乐观锁,大多是基于数据版本( Version )记录机制实现。)

2)开销问题:CAS操作长期不成功,cpu不断循环。

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

原子操作类的使用:
JDK中相关原子操作类的使用:
1)更新基本类型:AtomicBoolean, AtomicInteger, AtomicLong
简单的代码示例(AtomicInteger)

相关的方法也比较简单:getAndIncrement() 先取值再加1
incrementAndGet() 先加1再取值

package 原子操作;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lenovo
 */
public class UseAtomicInt {

    static AtomicInteger ai=new AtomicInteger(10);

    public static void main(String[] args) {
        //先取值再加1
        System.out.println(ai.getAndIncrement());
        //先加1再取值
        System.out.println(ai.incrementAndGet());
        System.out.println(ai.get());
    }
}

结果展示:
Java并发编程---原子操作CAS
2) 更新数组类:AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
简单代码示例(AtomicIntegerArray)
在进行值的更改时,其实是进行了一个数组的拷贝,并重新赋值,所以原数组的值并没有改变。

package 原子操作;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @author lenovo
 */
public class AtomicArray {

    static int []value=new int[]{1,2};

    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //第一个参数代表原数组的下标值 第二个参数代表值
        ai.getAndSet(0,3);
        System.out.println(ai.get(0));
        System.out.println(value[0]);

    }
}

结果展示(第一个结果表示改后新数组的值,第二个结果表示的是原数组的值)
Java并发编程---原子操作CAS
3) 更新引用类型:AtomicReference, AtomicMarkableReference, AtomicStampedReference
引用类型的原子操作类可以封装多个数据,已达到多个变量的原子操作
简单代码示例 (AtomicReference) :
UserInfo这个类封装了姓名和年龄这两个变量,进行值改变的时候,也是重新进行拷贝,再进行值的改变。

package 原子操作;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author lenovo
 * 类说明:演示引用类型的原子操作类
 */
public class UseAtomicReference {

    static AtomicReference<UserInfo> userRef=new AtomicReference<UserInfo>();

    public static void main(String[] args) {
        UserInfo user=new UserInfo("zs",15);
        userRef.set(user);

        UserInfo updateUser=new UserInfo("ls",17);
        //这是候的更改是在新生成得对象进行变更
        userRef.compareAndSet(user,updateUser);
        System.out.println(userRef.get().getName());
        System.out.println(userRef.get().getAge());
        System.out.println(user.getName());
        System.out.println(user.getAge());


    }

    static class UserInfo{
        private String name;
        private int age;

        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }


    }
}


结果展示(前两个值表示改变后引用的值,后两个值表示原来引用的值):
Java并发编程---原子操作CAS

4) 更新字段类:AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, AtomicLongFieldUpdater (很少使用)

解决ABA问题的两个类: AtomicMarkableReference 处理版本使用的是boolean (表示有没有动过)
AtomicStampedReference: 表示动过几次(它的构造函数为AtomicStampedReference(V initialRef, int initialStamp ) 第一个参数表示引用类型的值,第二参数表示版本号 )

简单代码示例(AtomicStampedReference具有较强的实用性,表示的是当我们遇到ABA问题时可采用版本号进行分辨后操作)
该代码中有两个线程对带有版本戳的类进行更改,其中一个线程是在版本正确的情况对引用类型的值进行更改(这种情况下是可以更改成功的)另外一个线程是在版本不正确的情况下对引用类型的值进行更改(这种情况下是更改失败的)
compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) 返回的结果值是Boolean表示更改成功或者失败
其中第一个参数表示期待的引用值,第二个表示新的引用值。
第三个参数表示的是期待的版本戳,第四个表示新的版本戳

package 原子操作;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author lenovo
 * 演示带版本戳的原子操作类
 */
public class UseAtomicStampedReference {

    //AtomicStampedReference(V initialRef, int initialStamp) 传初始值和版本号
    static AtomicStampedReference<String> asr=
            new AtomicStampedReference<String>("Mark",0);

    public static void main(String[] args) throws InterruptedException {
        final int oldStamp=asr.getStamp();
        final String oldReference=asr.getReference();

        System.out.println(oldReference+"======"+oldStamp);

        //正确版本号的线程
        Thread rightStampThread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+
                        "当前变量值为:"+oldReference+" 当前版本号为:"+oldStamp+"-"
                        +asr.compareAndSet(oldReference,oldReference+" Java",
                        oldStamp,oldStamp+1));


            }
        });

        //错误版本号的线程
        Thread errorStampThread=new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println(Thread.currentThread().getName()+
                        "当前变量值为:"+asr.getReference()+" 当前版本号为:"+asr.getStamp()+"-"
                        +asr.compareAndSet(asr.getReference(),asr.getReference()+" C",
                        oldStamp,oldStamp+1));


            }
        });

        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();
        System.out.println(asr.getReference()+"======"+asr.getStamp());
    }
}

结果展示(第一行的值表示的是原本引用类型的值和版本号 第二行的值表示的是一个在正确版本戳下对引用类型值的改变
第三行的值表示的是在一个错误版本戳下对引用值类型的改变,第四行代表的更改后最终的结果值)
Java并发编程---原子操作CAS

相关标签: Java