JDK源码阅读,手写AtomicInteger

关于java并发的三大特性,原子性、可见性、有序性关乎线程安全问题的基本原理,JDK提供java.util.concurrent.atomic包来对数据类型进行包装,以实现各数据类型的原子性操作。
关于三大特性与volatile关键字可参考文章深入理解volatile关键字
接下来就通过一道简单的题目层层解读AtomicInteger的底层原理。

demo1:i++的原子性问题

public class AtomicIntegerDemo1 {
    private int value = 0;

    public void add() {
        value++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo1 atomicIntegerDemo1 = new AtomicIntegerDemo1();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo1.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有线程执行完毕再输出
        System.out.println(atomicIntegerDemo1.value);
    }
}

这是一道简单的题目,创建20个线程,每个线程对共享变量value进行1000次自加操作。不论运行多少次,结果都不是我们期望的20000:
在这里插入图片描述
究其原因,假设现在value值为1,线程1将变量value的值从堆空间(线程共享)读取到虚拟机栈(线程私有)中进行自加操作value值变为2,再将新值2刷新到堆内存。这个过程中可能线程1还没来得及刷新到堆内存,线程2也读取value值到自己的栈内存,然后将新值2刷新到堆内存。这样两个线程对变量value一共自加两次,value值应当是3,而实际value的值仍然是2。这就是线程安全问题。本质原因还是因为value++操作是非原子操作。
要实现线程安全方法有两种:

  • 同步阻塞:通过sychronized关键字或者Lock对象加锁,public void sychronized add() { value++; },保证add()方法的原子性。同步阻塞方式优点是安全,实现简单,缺点是并发效率低。
  • 非阻塞并发:通过CAS(比较并交换)机制实现乐观锁,只有当当前值与期望值一样时,才将旧值替换为当前值,否则重新读取当前值,这种循环重试机制也叫自旋锁。异步并发的优点是性能高,CPU利用率高,线程持续运行,省去了CPU线程调度的性能消耗,缺点是如果当前值一直不与期望值一样,将会一直循环重试。乐观锁还可能出现ABA问题。

以上代码的运行结果并不会是我们期望的20000。将add()方法设置为同步方法可以解决问题,那再来看看AtomicInteger如何解决原子性问题的。
注意:以上代码中给value属性添加volatile关键字并不能解决问题,因为volatile关键字只有保证可见性和有序性的语义,而不能保证原子性。

demo2:使用AtomicInteger类

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo2 {
    private AtomicInteger value = new AtomicInteger();

    public void add() {
        value.getAndAdd(1);
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo2 atomicIntegerDemo2 = new AtomicIntegerDemo2();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo2.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有线程执行完毕再输出
        System.out.println(atomicIntegerDemo2.value);
    }
}

用AtomicInteger类对象的getAndAdd()方法代替int类型的i++操作,最后输出结果是我们期望的20000.
在这里插入图片描述
那么,AtomicInteger是如何实现原子性运算的呢。点开jdk源码
查看AtomicInteger的getAndAdd()方法:

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

有一个unsafe对象,

private static final Unsafe unsafe = Unsafe.getUnsafe();

这个unsafe对象所属类Unsafe提供了许多native方法,比如

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

用getIntVolatile()在底层通过对象(var1)和偏移量(var2)获取变量内存地址,直接通过内存地址而不是符号引用得到变量的当前值。有了对象(var1),属性地址偏移量(var2),旧值(var5 ),加上期望值(var5 + var4),就可以调用CAS操作(compareAndSwapInt方法)。unsafe对象的getAndAddInt方法如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);//第4行
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//第5行
        return var5;
    }

当当前线程执行第4行后,变量值被其他线程刷新,则当前线程执行第5行方法返回false,重新执行第4行获取值,直到当前值与期望值一样,才修改变量的值并返回新值。
看完了源码想仿造源码自己动手写一下AtomicInteger以加深印象。

demo3:仿造源码实现的AtomicInteger类

import sun.misc.Unsafe;

public class AtomicIntegerDemo3 {
    private volatile int value;
    //提供底层CAS操作的对象
    private static final Unsafe unsafe=Unsafe.getUnsafe();
    //属性在内存中相对于对象的地址偏移量
    private static final long valueOffset;
    static{
        try {
            //通过unsafe对象提供的方法获取value属性偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicIntegerDemo3.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    /**
     * CAS操作方法
     * @param current 旧值
     * @param update 期望值
     * @return 修改是否成功
     */
    public boolean compareAndSet(int current,int update){
        return unsafe.compareAndSwapInt(this,valueOffset,current,update);
    }

    //相当于AtomicInteger中getAndAdd()方法:将给定的值原子地添加到当前值
    public void add() {
        int current;
        do {
            //线程获取当前值
            current = unsafe.getIntVolatile(this,valueOffset);
        }while (!compareAndSet(current,current+1));//修改失败则重试
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo3 atomicIntegerDemo3 = new AtomicIntegerDemo3();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo3.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有线程执行完毕再输出
        System.out.println(atomicIntegerDemo3.value);
    }
}

想法很简单,拿到底层内存地址操作相关的底层Unsafe类的实例,根据本类的value属性用unsafe对象的objectFieldOffset()方法获取属性相对本类实例的地址偏移量,然后通过循环CAS操作改变属性值。
但是运行时报了异常:
在这里插入图片描述
第8行private static final Unsafe unsafe=Unsafe.getUnsafe();报错,点开getUnsafe()方法,

	@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

再点开VM.isSystemDomainLoader()方法:

public static boolean isSystemDomainLoader(ClassLoader var0) {
        return var0 == null;
    }

可以看出为直接调用getUnsafe()方法是不安全的,sunjdk设定只要调用类类加载器不为空,则抛出异常并提示Unsafe,也就是该方法只能虚拟机自己调用。
既然我们不能直接调用getUnsafe()方法来获取Unsafe的实例,那就用反射来获取吧。

demo4:手写AtomicInteger

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class AtomicIntegerDemo4 {
    private volatile int value;
    //提供底层CAS操作的对象
    private static final Unsafe unsafe;
    //属性在内存中相对于对象的地址偏移量
    private static final long valueOffset;
    static{
        try {
            //getUnsafe()方法不好使,通过反射获取unsafe对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //Unsafe类中的theUnsafe属性为private,设置可访问权限
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //通过unsafe对象提供的方法获取value属性偏移量
            valueOffset = unsafe.objectFieldOffset
                    (AtomicIntegerDemo4.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    /**
     * CAS操作方法
     * @param current 旧值
     * @param update 期望值
     * @return 修改是否成功
     */
    public boolean compareAndSet(int current,int update){
        return unsafe.compareAndSwapInt(this,valueOffset,current,update);
    }

    //相当于AtomicInteger中getAndAdd()方法
    public void add() {
        int current;
        do {
            //线程获取当前值
            current = unsafe.getIntVolatile(this,valueOffset);
        }while (!compareAndSet(current,current+1));//修改失败则重试
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo4 atomicIntegerDemo4 = new AtomicIntegerDemo4();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo4.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有线程执行完毕再输出
        System.out.println(atomicIntegerDemo4.value);
    }
}

修改过后通过反射获取unsafe对象,运行成功,能够正确输出20000:
在这里插入图片描述
虽然只是完成了getAndAdd()一个方法,但是通过阅读源码和仿造手写对Atomic原子类的原理有了一定的了解。
另外,测试发现,把value属性的volatile关键字去掉也不影响结果,jdk源码添加的关键字应该是不会多余的,猜测是与unsafe.getIntVolatile()这个方法有关,但是这是个native方法不能查看。
望有大神不吝赐教0.0

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/87904777
今日推荐