目录

T の weblog

X

Java中的Atomic

1. 代码中的加法

1+1=2,这是一个数学里普通的加法操作,那么使用代码进行展现的话如下所示

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

使用的时候需要先进行对象实例化,然后调用方法。这没什么问题,但是
当多线程的时候,情况可能变的不一样,我创建5个线程同时对Counter对象进行调用increment()方法1000次

public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        Counter counter = new Counter();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();
        System.out.println(counter.value());
    }

得到的结果是4960,而且每一次执行的结果还不一样,这当中发生了什么?
答案是 多个线程间没有保证内存操作的原子性

2. 原子操作

要弄懂上面说的答案 ,我们先需要知道,什么是原子操作。

Atomic,英文翻译为原子的,发音为 [əˈtɒmɪk]

原子(atom)本意是不能进一步分割的最小粒子,那么扩展一下,原子操作,那么就意味着“不可被中断的一个或者一系列操作”。

先来看一个不是原子性操作的例子,如果i=1,我们进行两次i++操作,我们的期望的结果是3,但是结果有可能是2。

原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行+1操作,然后分别写入系统内存中。

怎么解决?聪明的你肯定想到了,给方法上加上synchronized,像这样

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

或者使用显示锁

public class Counter {
        private int value;
        private Lock lock = new ReentrantLock();

        public void increment() {
            lock.lock();
            value++;
            lock.unlock();
        }

        public void decrement() {
            lock.lock();
            value--;
            lock.unlock();
        }

        public synchronized int get() {
            return value;
        }
    }

当然没有问题,但是对于更复杂的类,我们可能要避免不必要的同步对活动性的影响。用AtomicInteger替换int字段使我们能够防止线程干扰而无需求助于同步。

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

3. Java中的Atomic

在Java的java.util.concurrent.atomic包下,有17个类,其中

基本类:AtomicInteger AtomicLong AtomicBoolean
引用类型:AtomicReference AtomicStampedReference AtomicMarkableReference
数组类型:AtomicIntegerArray AtomicLongArray AtomicReferenceArray
属性原子修改器:AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater

可以通过原子的方式更新对应的值。

上面提到的是12个,自Java8以来引入了4个新的计数器类型 DoubleAccumulator DoubleAdder LongAccumulator LongAdder

那么在LongAdder 与AtomicLong有什么区别?

LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。

它们都是怎么做到原子操作的?

3.1 原理

我们点进AtomicInteger源码进行查看
它的值是存在一个volatile的int里面。volatile只能保证这个变量的可见性。不能保证他的原子性。

看看getAndIncrement这个类似i++的函数,可以发现,是调用了UnSafe中的getAndAddInt

那他如何保证其原子性呢?

自旋 + CAS(乐观锁)。在这个过程中,通过compareAndSwapInt比较更新value值,如果更新失败,重新获取旧值,然后更新。

AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中的value内存地址,从而可以根据CAS实现对value字段的原子操作

3.2 实际应用

对于这个Atomic,在实际应用中,可以完成哪些功能?

3.2.1 多线程安全的全局唯一ID生成器

class IdGenerator {
    AtomicLong var = new AtomicLong(0);

    public long getNextId() {
        return var.incrementAndGet();
    }
}

3.2.2 限制并发流量,用来限制接口的流量,超过并发的数量的阈值进行熔断

3.3 Atomic优缺点

优点: CAS相对于其他锁,不会进行内核态操作,有着一些性能的提升
缺点: 引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu资源会消耗很高。

参考文章:
Atomic Variables
Understanding Atomic Variables in Java
使用Atomic
Java并发编程包中atomic的实现原理


标题:Java中的Atomic
作者:MingGH
地址:https://runnable.run/articles/2021/03/12/1615543696270.html