没用过Java原子类?我来手写一个AtomicInteger

2022-05-05 17:03:06 浏览数 (1)

问题引出

大家可能听过「Automic」原子类,这些类在多线程下可以保证线程安全。比如想要实现自增,多线程下会出现少增加的情况。

代码语言:javascript复制
public class VolatileAtomicTest {
    public static int num = 0;

    public  static void increase() {
        num  ;
    }  

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i  ) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j  ) {
                    increase();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println(num);
    }
}

上面代码创建了10个线程,这10个线程要依次循环1000次,每次增加1,最后要求num = 10000。

但实际情况是

多线程情况下会出现第一个线程还在运算,第二个线程就运算完并覆盖了第一个线程的值运算结果,所以会出现与预期不符的结果。原因在

代码语言:javascript复制
public  static void increase() {
        num  ;
} 

num 我们可以拆解为

代码语言:javascript复制
int a = num   1;   //步骤1
num = a;           //步骤2

如果线程一只执行到步骤1,还没执行到步骤2,线程二这时执行了步骤2。那么num的值就是线程2计算的值,而线程一的值就覆盖了。

如果我们保证这两步的原子性(操作一体,不能被其他线程插入)就可以得到预期结果。我们在这个方法下面加锁即可。

代码语言:javascript复制
public synchronized static void increase() {
    num  ;
}

或者

代码语言:javascript复制
static Lock lock = new ReentrantLock();

public static void increase() {
    try {
        lock.lock();
        num  ;
    } finally {
        lock.unlock();
    }
} 

但是,上面的方法都使用了锁,在多线程下对性能还是有影响的。我们可以使用无锁化的原子类,实现原子自增。

代码语言:javascript复制
@Test
public void test() throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i  ) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000; j  ) {
                num = atomicInteger.incrementAndGet();
            }
        });
        threads[i].start();
    }
    for (Thread thread : threads) {
        thread.join();
    }
    System.out.println(num);
}

原子类

上图就是Java原子类的全家桶,主要是通过CAS 自旋实现的。这里我们主要说说AtomicInteger,来看看incrementAndGet()方法。Java8增加了一些类,优化自选带来的性能问题。

代码语言:javascript复制
/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
代码语言:javascript复制
/ setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

发现用了UnsafegetAndAddInt方法。至于Unsafe,底层也是操作系统的类,可以直接修改操作系统内存,或者调度线程。看着名字就知道不建议程序员使用它。

代码语言:javascript复制
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);//从该对象对应地址取出变量值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5   var4));

    return var5;
}
  • var1: Object
  • var2: valueOffset
  • var5: 当前该变量在内存中的值
  • var5 var4: 需要写进去的值

这里就是通过CAS修改值,不断循环,知道修改成功。但在高并发情况下,存在一些问题:

❝高并发量的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间,并且浪费线程资源。 由于需要保证变量真正的共享,「缓存一致性」开销变大。 ❞

getIntVolatile()方法是系统的本地方法

代码语言:javascript复制
public native int getIntVolatile(Object var1, long var2);

compareAndSwapInt()也是本地方法,这里就是常说的CAS了。

代码语言:javascript复制
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

手写AtomicInteger

「首先定义几个变量」

代码语言:javascript复制
private volatile int value;

private static long offset;//偏移地址

private static Unsafe unsafe;

我们定义了value值,用来保存当前的值。offset偏移地址,在类初始化的时候,计算出value变量在对象中的偏移。Unsafe类,直接操作系统内存。

「初始化变量」

代码语言:javascript复制
// 通过Unsafe计算出value变量在对象中的偏移
static {
    try {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        unsafe = (Unsafe) theUnsafeField.get(null);
        Field field = MyAtomicInteger.class.getDeclaredField("value");
        offset = unsafe.objectFieldOffset(field);//获得偏移地址
    } catch (Exception e) {
        e.printStackTrace();
    }
}

我们反射获取UnsafetheUnsafe字段,自定义的value字段,还有偏移地址offset

「自增方法」

代码语言:javascript复制
public void increment(int num) {
    int tempValue;
    do {
        tempValue = unsafe.getIntVolatile(this, offset);//拿到值
    } while (!unsafe.compareAndSwapInt(this, offset, tempValue, value   num));//CAS自旋
}

「测试结果」

代码语言:javascript复制
public class MyAtomTest {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        MyAtomicInteger atomicInteger = new MyAtomicInteger();
        for (int i = 0; i < 10; i  ) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j  ) {
                    atomicInteger.increment(1);  //自增1
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i  ) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("x="   atomicInteger.get());
    }
}

「Atomic」原子类解决了高并发下线程安全问题,但是高并发下也带来了性能问题,如果你的项目需要使用原子类,并且性能要求高,可以使用「Java8」中的原子类。

0 人点赞