天天看点

没用过Java原子类?我来手写一个AtomicInteger往期推荐

问题引出

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

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,最后要求。

但实际情况是

没用过Java原子类?我来手写一个AtomicInteger往期推荐

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

public  static void increase() {
        num++;
} 
           

num++

我们可以拆解为

int a = num + 1;   //步骤1
num = a;           //步骤2
           

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

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

public synchronized static void increase() {
    num++;
}
           

或者

static Lock lock = new ReentrantLock();

public static void increase() {
    try {
        lock.lock();
        num++;
    } finally {
        lock.unlock();
    }
} 
           
没用过Java原子类?我来手写一个AtomicInteger往期推荐

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

@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原子类?我来手写一个AtomicInteger往期推荐

原子类

没用过Java原子类?我来手写一个AtomicInteger往期推荐

上图就是Java原子类的全家桶,主要是通过CAS + 自旋实现的。这里我们主要说说

AtomicInteger

,来看看

incrementAndGet()

方法。Java8增加了一些类,优化自选带来的性能问题。

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

发现用了

Unsafe

getAndAddInt

方法。至于

Unsafe

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

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()

方法是系统的本地方法

public native int getIntVolatile(Object var1, long var2);
           

compareAndSwapInt()

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

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
           

手写

AtomicInteger

「首先定义几个变量」

private volatile int value;

private static long offset;//偏移地址

private static Unsafe unsafe;
           

我们定义了

value

值,用来保存当前的值。offset偏移地址,在类初始化的时候,计算出value变量在对象中的偏移。

Unsafe

类,直接操作系统内存。

「初始化变量」

// 通过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();
    }
}
           

我们反射获取

Unsafe

theUnsafe

字段,自定义的

value

字段,还有偏移地址

offset

「自增方法」

public void increment(int num) {
    int tempValue;
    do {
        tempValue = unsafe.getIntVolatile(this, offset);//拿到值
    } while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
}
           

「测试结果」

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());
    }
}
           
没用过Java原子类?我来手写一个AtomicInteger往期推荐

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

往期推荐

  • 我写出这样干净的代码,老板直夸我
  • 云南丽江旅游攻略
  • 使用ThreadLocal怕内存泄漏?
  • Java进阶之路思维导图
  • 程序员必看书籍推荐
  • 3万字的Java后端面试总结(附PDF)

扫码二维码,获取更多精彩。或微信搜Lvshen_9,可后台回复获取资料

1.回复"java" 获取java电子书;


2.回复"python"获取python电子书;


3.回复"算法"获取算法电子书;


4.回复"大数据"获取大数据电子书;


5.回复"spring"获取SpringBoot的学习视频。


6.回复"面试"获取一线大厂面试资料


7.回复"进阶之路"获取Java进阶之路的思维导图


8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版)


9.回复"总结"获取Java后端面试经验总结PDF版


10.回复"Redis"获取Redis命令手册,和Redis专项面试习题(PDF)
           

另:点击【我的福利】有更多惊喜哦。

没用过Java原子类?我来手写一个AtomicInteger往期推荐