• Java多线程系列之:原子操作CAS


    一,什么是原子操作?如何实现原子操作

    1,synchronized可以完成原子操作,他是给予阻塞的锁的机制,但是有问题:

      如果被阻塞的线程优先级很高怎么办?
      拿到锁的线程一直不释放锁怎么办?
      有大量线程进行竞争,消耗cpu。还容易出现死锁
      锁的粒度比较大,影响性能。

    二,CAS的原理(Compare And Swap:比较交换)

    1,从指令级别保证这是一个原子操作。

    每个CAS都包含三个运算符:
      一个内存地址V
      一个期望的值A
      一个新值B

    基本思路:
      如果在内存地址V上进行操作,如果说这个地址上存放的值就是我期望的值A。就给地址V赋给新值B.
      如果内存地址上不是我期待的A值,那就什么都不做。
      在循环(这里用的是死循环,不带条件的一直在那里循环。又叫:自旋)里不断的进行CAS操作

    利用了现代操作系统都支持CAS的指令,循环这个指令,直到成功为止。
    其实是不再语言层面进行处理,而是把它交给了cpu和内存去实现,利用cpu的多处理能力,在硬件层面实现多线程安全。

    三,CAS的问题

    1,ABA问题

    问题描述:
      第一个线程拿到内存地址上的值。此时第二个线程也拿到内存地址上的值,然后修改成B,然后又改回去。
      在第二个线程完成前面一顿操作之后,第一个线程才开始比较内存地址上的是否和A相等。那么比较的结果
      肯定是相同的啦。第二个线程,修改过值,又修改回去。但是对于第二个线程的那些操作,第一个线程却不知道,这是有风险的。

    怎么解决:引入版本号,每次变化,版本号都变化。这样每次数据变化都可以感知到

    2,开销问题

      在死循环里不断的进行操作,如果这个操作长期不成功,浪费cpu资源

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

      一个内存地址只能指向一个变量,当有多个共享变量时,就无法保证操作的原子性。
      那么如果有多个共享变量呢?我们可以把多个共享变量变成一个共享变量,比如把多个变量封装到一个类中。

    四,JDK中相关原子操作类的使用

    1,更新基本类型:

      AtomicBoolean,AtomicInteger,AtomicLong

    2,更新数组类:

      AtomicInterArray,AtomicLongArray,AtomicReferenceArray

    3,更新引用类型:

      AtomicReference,AtomicMarkableReference,AtomicStampedReference

    4,原子更新字段类:

      AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

    五,实际使用

    1,AtomicIntegerArray

    public class UseAtomicArray {
    
        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]);
        }
    }

    2,AtomicReference

    /**
     * 引用类型的原子操作类
     */
    public class UseAtomicReference {
    
        static AtomicReference<UserInfo> userRef = new AtomicReference<>();
    
        public static void main(String[] args) {
            UserInfo userInfo = new UserInfo("Mark",12);
            userRef.set(userInfo);
    
            UserInfo udpateUser = new UserInfo("Bill",14);
            userRef.compareAndSet(userInfo,udpateUser);
            //引用的对象被修改了
            System.out.println(userRef.get().getName()+":"+userRef.get().getAge());
    
            //原对象的值并没有改变
            System.out.println(userInfo.getName()+":"+userInfo.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 void setName(String name) {
                this.name = name;
            }
    
            public int getAge() {
                return age;
            }
    
            public void setAge(int age) {
                this.age = age;
            }
        }
    }

    3,AtomicStampedReference

    /**
     * 带版本戳的原子操作类
     */
    public class UseAtomicstampedReference {
    
        static AtomicStampedReference<String> asr = new AtomicStampedReference<>("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() {
                    String reference = asr.getReference();//重新获取reference
                    int stamp = asr.getStamp();
                    System.out.println(Thread.currentThread().getName()+"当前变量值:"+reference+
                            "当前版本戳:"+stamp+"-"+asr.compareAndSet(reference,
                            reference+"+C",oldStamp,oldStamp+1));
                }
            });
    
            rightStampThread.start();
            //确保让rightStampThread先执行
            rightStampThread.join();
    
            //第二次修改会失败,因为上面的线程已经修改过一次了,再次在旧的版本号上修改是无法成功的
            errorStampThread.start();
            errorStampThread.join();
    
            System.out.println("finally--->"+asr.getReference()+"-"+asr.getStamp());
    
        }
    }
  • 相关阅读:
    北京大学数学分析习题集参考解答03.07难题
    北京大学数学分析习题集参考解答03.06一致连续性
    北京大学数学分析习题集参考解答03.05最大、最小值
    北京大学数学分析习题集参考解答03.03中间值性质03.04初等函数的连续性
    北京大学数学分析习题集参考解答03.02连续函数的运算
    北京大学数学分析习题集参考解答03.01连续与间断
    [Oracle工程师手记] 利用 DBMS_SQLTUNE.report_sql_monitor 生成 SQL 语句的监控信息
    [Oracle工程师手记] Data Guard 环境中,查找最近发生的与 Data Guard 相关的错误的方法
    [Oracle 工程师手记] Windows 环境下,获取与 oracle 相关 registry 的小技巧
    [Oracle工程师手记]从RAC环境备份后向新环境(文件系统)恢复的试验
  • 原文地址:https://www.cnblogs.com/inspred/p/11103575.html
Copyright © 2020-2023  润新知