• Atomic底层原理


    JDK提供了一些原子操作的类,在java.util.concurrent.atomic下面。如AtomicBoolean,AtomicInteger,AtomicLong都是用原子的方式来更新指定类型的值。

    Unsafe类包含了大量多的对C代码的操作,包括了很多直接内存分配和原子操作的调用,都存在安全隐患,所以标记为unsafe。

    AtomicInteger是一个标准的乐观锁实现,sun.minc.Unsafe是JDK提供的乐观锁的支持

    为什么需要Atomic类?

    实例代码

    public class Dome01 {
        private Integer count = 0;
       
        private static int min = 10;//每次加10次
        private static int max = 100;//有100个线程同时加
        
        public Integer getCount() {
            return count;
        }
       
        public void addCount(){
            for (int i = 0; i < min; i++) {
                count++;
            }
        }
        @Test
        public void testInteger(){
            Long temp = System.currentTimeMillis();
            Dome01 dome01 = new Dome01();
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < max; i++) {
                executorService.execute(()->{
                    dome01.addCount();
                });
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("testInteger --- >"+dome01.getCount() +"---- 执行时间 ---- "+(System.currentTimeMillis() - temp));
            executorService.shutdown();
        }
    
    }
    

    执行结果:每次都不一样,往往达不到预期想要的值,而且并发量越大相差的越远。

    我们可以看出Integer类型的count在高并发的情况并不能保证线程安全。我们一共执行了1000次,但结果往往是一个小于1000的值。原因在于count++并不是一个原子性操作(读取--运算--赋值)。

    如何解决

    加锁

    synchronized

    Lock lock = new ReentrantLock();
    public synchronized void addCount(){
        for (int i = 0; i < min; i++) {
            count++;
        }
    }
    

    既然是因为count++不是原子操作造成的,那么最容易想到的就是使用synchronized对方法进行加锁,但是我只想保证count++是原子操作,使用synchronized有种高射炮打蚊子的感觉。

    ReentrantLock

    Lock lock = new ReentrantLock();
    public void addCount(){
        for (int i = 0; i < min; i++) {
            lock.lock();
            count++;
            lock.unlock();
        }
    }
    

    进一步优化,只对count++操作进行加锁,但是虽然比synchronized性能要好些,但是还有一种杀鸡用牛刀的感觉。

    使用CAS

    虽然使用synchronized和ReentrantLock都可以解决这个问题,但是总是感觉不够理想,于是引入了CAS。

    什么是CAS

    CAS是一种乐观锁实现,有三个值 V 表示当前内存的值、A 表示预期的旧值、 B 表示将要更新的新值。

    CAS的核心原理在于当要更新值得时候就进行compareAndSwap,而这个compareAndSwap操作是一个native方法,底层是用C/C++写的。最终调用的是cmpxchg方法,其保证了CAS操作的原子性。

    compxchg(&a,a,b);//调用compxchg函数
    //伪代码
    bool cmpxchg(int *v,int a,int b){
        lock();
        if(*v == a){
            *v = b;
            return true;
        }else{//也可以进行自旋
            return false;
        }
        unlock();
    }
    

    以上是个人猜测的cmpxchg函数的实现,不然的话,根本无法保证线程安全

    假设在多线程环境下,有A,B两个线程同时执行了第一行。同时调用了compxchg(&a,a=10,b=20),由于加了lock只有个线程能执行,线程A判断 if(*v(10) == a(10))成功,然后将*v值修改为20,线程A执行结束释放lock;线程B进入开始执行判断 if(*v(20)==a(10))失败。这样既保证了cas操作的原子性,又保证了线程安全。由于Java没有指针这个函数用Java是实现不了的,这就是C语言的魅力。

    CAS解决

    现在可以使用CAS来轻量级的解决上面问题。很多复杂的直接对内存的操作Java做不到的,不过JDK提供了一个Unsafe包,里面分装了很多强大的操作。

    到此就很优雅的解决了上面提到的count++非原子性的,为了避免重复造轮子,所以JDK将这些CAS操作都分装到了Atomic类中了。

    Atomic类有哪些?

    Atomic类都放在java.util.concurrent.atomic下面的,如图:前面12个是JDK8之前的,后面5个是JDK8之后的。

    基本类型

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

    • AtomicInteger:整形原子类
    • AtomicLong:长整形原子类
    • AtomicBoolean:布尔原子类
    数组类型

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

    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray:引用类型数组原子类(数组中存放对象)
    引用类型

    使用原子的方式更新某个对象

    • AtomicReference:引用类型原子类
    • AtomicStampedReference:AtomicReference的扩展版,增加了一个参数stamp标记,这里是为了解决了AtomicInteger和AtomicLong的操作会出现ABA问题。
    • AtomicMarkableReference :与AtomicStampedReference差不多,只不过第二个参数不是用的int作为标志,而用boolean类型做标记
    对象的属性修改类型

    使用原子的方式更新某个类中某个字段

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    • AtomicLongFieldUpdater:原子更新长整形字段的更新器
    • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

    LongAdder

    是什么?

    LongAdder同样是原子类,AtomicInteger在高并发场景下,会大量自旋带来性能浪费,于是引入了LongAdder类,解决大量自旋的问题。

    原理是什么?

    由于AtomicInteger类在高并发场景下,会有大量线程要修改同一个值,但同一时刻只有一个线程可以修改成功。其他线程就会自旋来等待。当有大量自旋操作时,是对CPU资源的一种浪费。

    LongAdder内部维护了一个Call[]数组,将核心的数据vulue分离到数组的各个位置,每个线程访问时,通过哈希等算法映射到其中一个数字进行运算。而最终的运算结果,则是对Call[]数组中的所有值进行累加求和

    总的来说,就是将一个值,分散为不同的值,在并发的时候可以分散压力,在高并发下,可以减少自旋次数,可以显著的提升性能。

    ABA问题

    举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了,然后觉得不太好,又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。

    如何解决ABA问题

    可以加一个版本号,每次修改的时候版本号加一,这样就不存ABA的问题了,我在更新之前,除了要确保值没有被修改还要确保版本号没被修改。

    思考

    我引入了版本号之后,还有必要判断值是原来的值吗?直接根据版本号不就知道了值有木有修改了吗?

    个人想法

    我们的本意是修改值,重点在于保证值得正确修改,如果把注意点偏移到版本号上,有本末倒置的嫌疑。而且若真的要根据版本号来判断值是否被修改,还需要维护版本号更新的安全性。

  • 相关阅读:
    亚信防毒墙网络版卸载
    Ubuntu之apt
    Python(00):内存中读写数据StringIO和BytesIO
    Windows使用cmd命令行中查看、修改、删除与添加环境变量
    微信小程序教程
    微信小程序之云开发
    微信小程序-简易计算器
    第一个微信小程序——实现获取用户信息替换用户名和头像到首页
    Python(00):RSA加解密
    Python(00):使用selenium模块
  • 原文地址:https://www.cnblogs.com/shaoyu/p/14659980.html
Copyright © 2020-2023  润新知