zoukankan      html  css  js  c++  java
  • JUC 中的 Atomic 原子类

     1.Atomic原子类概述

    Java1.5的Atomic包名为java.util.concurrent.atomic。

    这个包提供了一系列原子类。

    这些类可以保证多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行

    Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的

    原子类内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法(同步的工作更多的交给了硬件),从而避免了synchronized的高开销,执行效率大为提升。

    虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了

    什么是CAS

           CAS,Compare and Swap即比较并交换。

    java.util.concurrent包借助CAS实现了区别于synchronized同步锁的一种乐观锁。

    乐观锁就是每次去取数据的时候都乐观的认为数据不会被修改,所以不会上锁,但是在更新的时候会判断一下在此期间数据有没有更新。

    CAS有3个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    CAS的关键点在于,系统在硬件层面保证了比较并交换操作的原子性,处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

    CAS的优缺点

      CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。

     CAS虽然很高效的实现了原子操作,但是它依然存在三个问题

      1、ABA问题。CAS在操作值的时候检查值是否已经变化,没有变化的情况下才会进行更新。

    但是如果一个值原来是A,变成B,又变成A,那么CAS进行检查时会认为这个值没有变化,但是实际上却变化了。

    ABA问题的解决方法是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就变成1A-2B-3A。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

      2、并发越高,失败的次数会越多,CAS如果长时间不成功,会极大的增加CPU的开销。因此CAS不适合竞争十分频繁的场景。

      3、只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。

    比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

    Atomic类的原理是什么呢

      一句话来说,atomic类是通过自旋CAS操作volatile变量实现的。使用volatile变量是为了多个线程间变量的值能及时同步。

    为什么使用Atomic类

    在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。

      而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一中实现方式。这种方式就避免了直接使用内核状态的重量级锁。

      但是在JDK1.6以后,synchronized进行了优化,引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。

    自旋:

    atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋

      通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。

    这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。

    因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。

    当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。    

     在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

    所以,所谓原子类说简单点就是具有原子/原子操作特征的类

     根据操作的数据类型,可以将JUC包中的原子类分为4类

    基本类型

    使用原子的方式更新基本类型

    • AtomicInteger:整型原子类

    • AtomicLong:长整型原子类

    • AtomicBoolean :布尔型原子类

    数组类型

    使用原子的方式更新数组里的某个元素

    • AtomicIntegerArray:整形数组原子类

    • AtomicLongArray:长整形数组原子类

    • AtomicReferenceArray :引用类型数组原子类

    引用类型

    • AtomicReference:引用类型原子类

    • AtomicStampedRerence:原子更新引用类型里的字段原子类

    • AtomicMarkableReference :原子更新带有标记位的引用类型

    对象的属性修改类型

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

    • AtomicLongFieldUpdater:原子更新长整形字段的更新器

    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

     如下:

    并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。

    下面我们来详细介绍一下这些原子类。

    2 基本类型原子类

    2.1 基本类型原子类介绍

    使用原子的方式更新基本类型

    • AtomicInteger:整型原子类

    • AtomicLong:长整型原子类

    • AtomicBoolean :布尔型原子类

    上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。

    AtomicInteger 类常用方法

    public final int get() //获取当前的值
    public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    public final int getAndIncrement()//获取当前的值,并自增
    public final int getAndDecrement() //获取当前的值,并自减
    public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
    public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

    2.2 AtomicInteger 常见方法使用

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerTest {
    
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            int temvalue = 0;
            AtomicInteger i = new AtomicInteger(0);
            temvalue = i.getAndSet(3);
            System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:0;  i:3
            temvalue = i.getAndIncrement();
            System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:3;  i:4
            temvalue = i.getAndAdd(5);
            System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:4;  i:9
        }
    
    }

    2.3 基本数据类型原子类的优势

    通过一个简单例子带大家看一下基本数据类型原子类的优势

    ①多线程环境不使用原子类保证线程安全(基本数据类型)

    class Test {
            private volatile int count = 0;
            //若要线程安全执行执行count++,需要加锁
            public synchronized void increment() {
                      count++; 
            }
    
            public int getCount() {
                      return count;
            }
    }

    ②多线程环境使用原子类保证线程安全(基本数据类型)

    class Test2 {
            private AtomicInteger count = new AtomicInteger();
    
            public void increment() {
                      count.incrementAndGet();
            }
          //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
           public int getCount() {
                    return count.get();
            }
    }

    2.4 AtomicInteger 线程安全原理简单分析

    AtomicInteger 类的部分源码:

        // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;

    AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升

    CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。

    UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。

    另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值

    3 数组类型原子类

    3.1 数组类型原子类介绍

    使用原子的方式更新数组里的某个元素

    • AtomicIntegerArray:整形数组原子类

    • AtomicLongArray:长整形数组原子类

    • AtomicReferenceArray :引用类型数组原子类

    上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。

    AtomicIntegerArray 类常用方法

    public final int get(int i) //获取 index=i 位置元素的值
    public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
    public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
    public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
    public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
    public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

    3.2 AtomicIntegerArray 常见方法使用

    import java.util.concurrent.atomic.AtomicIntegerArray;
    
    public class AtomicIntegerArrayTest {
    
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            int temvalue = 0;
            int[] nums = { 1, 2, 3, 4, 5, 6 };
            AtomicIntegerArray i = new AtomicIntegerArray(nums);
            for (int j = 0; j < nums.length; j++) {
                System.out.println(i.get(j));
            }
            temvalue = i.getAndSet(0, 2);
            System.out.println("temvalue:" + temvalue + ";  i:" + i);
            temvalue = i.getAndIncrement(0);
            System.out.println("temvalue:" + temvalue + ";  i:" + i);
            temvalue = i.getAndAdd(0, 5);
            System.out.println("temvalue:" + temvalue + ";  i:" + i);
        }
    
    }

    4 引用类型原子类

    4.1 引用类型原子类介绍

    基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。

    • AtomicReference:引用类型原子类

    • AtomicStampedRerence:原子更新引用类型里的字段原子类

    • AtomicMarkableReference :原子更新带有标记位的引用类型

    上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。

    4.2 AtomicReference 类使用示例

    import java.util.concurrent.atomic.AtomicReference;
    public class AtomicReferenceTest {
        public static void main(String[] args) {
            AtomicReference<Person> ar = new AtomicReference<Person>();
            Person person = new Person("SnailClimb", 22);
            ar.set(person);
            Person updatePerson = new Person("Daisy", 20);
            ar.compareAndSet(person, updatePerson);
    
            System.out.println(ar.get().getName());
            System.out.println(ar.get().getAge());
        }
    }
    
    class Person {
        private String name;
        private int age;
    
        public Person(String name, int age) {
            super();
            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;
        }
    
    }

    上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,

    然后调用 compareAndSet 方法,该方法就是通过通过 CAS 操作设置 ar。

    如果 ar 的值为 person 的话,则将其设置为 updatePerson。

    实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。

    5 对象的属性修改类型原子类

    5.1 对象的属性修改类型原子类介绍

    如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。

    • AtomicIntegerFieldUpdater:原子更新整型字段的更新器

    • AtomicLongFieldUpdater:原子更新长整型字段的更新器

    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

    要想原子地更新对象的属性需要两步。

      第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

      第二步,更新的对象属性必须使用 public volatile 修饰符

    上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerFieldUpdater为例子来介绍。

    5.2 AtomicIntegerFieldUpdater 类使用示例

    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
    
    public class AtomicIntegerFieldUpdaterTest {
        public static void main(String[] args) {
            AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
    
            User user = new User("Java", 22);
            System.out.println(a.getAndIncrement(user));// 22
            System.out.println(a.get(user));// 23
        }
    }
    
    class User {
        private String name;
        public volatile int age;
    
        public User(String name, int age) {
            super();
            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;
        }
    
    }

    https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484834&idx=1&sn=7d3835091af8125c13fc6db765f4c5bd&source=41#wechat_redirect

  • 相关阅读:
    [单选题]请求文件“time.inc”,当发生错误时就终止脚本,正确的方式是:
    [单选题]条件语句的时候不应该使用哪一种控制结构
    [高德地图]学习笔记--基本结构
    nodejs实战:小爬虫
    linux实用命令(2016/11/8-永远)
    自适应响应式布局-实现原理
    解决npm安装慢的方法
    git进阶(分支与标签管理)
    git进阶(远程仓库github)
    git入门命令(只涉及本地仓库管理)
  • 原文地址:https://www.cnblogs.com/Vincent-yuan/p/15022646.html
Copyright © 2011-2022 走看看