java原子性操作Atomic原理探究

一、代码引入

public class AtomicDemo {
    volatile int i = 0;
    public void add(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo ad = new AtomicDemo();
        for (int i = 0; i < 2; i++){
            new Thread( () ->{
                for (int j = 0; j < 10000; j++) {
                    ad.add();
                }
            }).start();
        }
        Thread.sleep(200L);
        System.out.println(ad.i);
    }
}

我们创建一个对象,new了两个线程,每一个线程执行10000遍,我们的预期结果应该是输出值为:20000。但是实际上输出的值总是小于20000的,为什么会出现这样的情况呢?我们先来看下面的图片:

java最后在底层执行i++这句代码的时候实际上是三个步骤。如上图所示。那么多线程的情况下,就会出现,线程1读取了1的值为1,开始进行计算操作,在线程1还没有执行结束的时候,线程2也读取了i的值,因为线程1 还没有执行结束,所以线程读取出来的i的值还是1.那么线程2执行的本次操作实际上无效的。

那么实际的业务中我们当然是希望可以达到预期结果的,那么我们如何让程序输出我们想要的预期结果呢?

这里就引出原子性操作的概念:

1、原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)

2、整个原子操作应该视为一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

那么我们才能保证这样的效果呢?有多种实现实现,比如说加锁,那么接下来我们看一下java都为我们提供了哪些可以解决这个问题的机制。

CAS机制

CAS(Conmpare And Swap)即比较和交换是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)

    JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下。UnSafe类中,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现了CAS机制。

CAS中有三个核心参数:

1.主内存中存放的V值,所有线程共享。

2.线程上次从内存中读取的V值A存放在线程的帧栈中,每个线程私有。

3.需要写入内存中并改写V值的B值。也就是线程对A值操作后放入到主存V中。

那么我们先用CAS机制来实现我们的预期结果,代码如下:

public class AtomicDemoCAS {
    volatile int value = 0;

    static Unsafe unsafe;//直接操作内存,通过内存中针对对象的属性的偏移量来修改对象的属性值
    private static long valueOffset;

    static{
        try {
            //通过反射获取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            //获取value属性偏移量(用于定位value属性在内存中的具体位置)
            valueOffset = unsafe.objectFieldOffset(AtomicDemoCAS.class.getDeclaredField("value"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void add(){
//        value++; 需要重点修改的地方,采用CAS机制来实现原子操作
        //记录本次操作中读取到的内存中的值
        int current;
        do {
            //直接通过偏移量去读取内存中的属性值。如果下面的操作执行失败了,说明内存中的值已经被其他线程更新,就重新通过偏移量获取一遍
            current = unsafe.getIntVolatile(this,valueOffset);
        }while(!unsafe.compareAndSwapInt(this,valueOffset,current,current+1));//可能会失败,失败返回的false
        //compareAndSwapInt()方法中:
        // 第一个属性指的是这个操作的属性属于哪个对象,因为是本对象所以是this
        // 第二个属性指的是偏移量
        // 第三个属性指的是当前内存中的值(会拿着这个去和最新的值比较)
        // 第四个属性指的是本次要赋值给属性的值
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemoCAS ad = new AtomicDemoCAS();
        for (int i = 0; i < 2; i++){
            new Thread( () ->{
                for (int j = 0; j < 10000; j++) {
                    ad.add();
                }
            }).start();
        }
        Thread.sleep(200L);
        System.out.println(ad.value);
    }
}

通过上面的代码示例,我们学习到了,如何通过java提供的底层的一些API来实现线程中的原子性操作。本质就是加了一个判断逻辑,在更新属性值之前做了一个判断,判断当前线程拿到的值是否已经被改变,如果被改变,则更新失败。

其实java已经为做了上面的操作,封装好了一些类,所以在J.U.C包下面,有一个原子操作类,我们可以直接使用,具体代码如下:

public class AtomicDemoAPI {
    AtomicInteger i = new AtomicInteger(0);

    public void add(){
        i.incrementAndGet();//i++操作
//        i.decrementAndGet();//i--操作
    }
    public static void main(String[] args) throws InterruptedException {
        AtomicDemoAPI ad = new AtomicDemoAPI();
        for (int i = 0; i < 2; i++){
            new Thread( () ->{
                for (int j = 0; j < 10000; j++) {
                    ad.add();
                }
            }).start();
        }
        Thread.sleep(200L);
        System.out.println(ad.i);
    }
}

如上用很少的代码实现了原子性操作。除了这些之外,还有一些其他的类型的值的原子性操作,都有如下图,大家可以自己去看。

三、引发思考

我们在了解了原子性操作的底层实现原理之后,我们思考一下,CAS原理实现的原子性操作会不会有什么问题?

我们知道他在每一次执行自增之前都会都会去判断,去比较,那么势必会造成CPU性能资源的消耗。所以说,java在jdk1.8的时候提出了计数器增强版,LongAdder、DoubleAdder等等。

增强版计数器在高并发情况下性能会更好,他的设计原理就是,简单点的说其实就是对于同一个变量的不断相加操作,实际上在内存里面是被分到了不同的操作单元,不同的线程做累加的时候操作不同的单元,然后需要读取的时候再由sum操作去把所有的操作单元的值相加起来,返回给方法。如下图所示,这样的设计就是分而治之。将我们要进行的操作分发到不同的操作单元,这样就可以降低不同线程之间的冲突,相对应的提高线程执行的效率。

思考:sum操作在做操作单元的累加的时候,是如何保证高并发的情况下,算出来的值是准确的呢?

java本身其实就是一个框架,所以我们可以学习他的设计思路和模式,运用到我们的业务代码中。

感兴趣的同学可以运行一下下面的这部分代码,测试一下不同的方式实现原子操作的效率。

// 测试用例: 同时运行2秒,检查谁的次数最多
public class LongAdderDemo {
    private long count = 0;

    // 同步代码块的方式
    public void testSync() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    synchronized (this) {
                        ++count;
                    }
                }
                long endtime = System.currentTimeMillis();
                System.out.println("SyncThread spend:" + (endtime - starttime) + "ms" + " v" + count);
            }).start();
        }
    }

    // Atomic方式
    private AtomicLong acount = new AtomicLong(0L);

    public void testAtomic() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    acount.incrementAndGet(); // acount++;
                }
                long endtime = System.currentTimeMillis();
                System.out.println("AtomicThread spend:" + (endtime - starttime) + "ms" + " v-" + acount.incrementAndGet());
            }).start();
        }
    }

    // LongAdder 方式
    private LongAdder lacount = new LongAdder();
    public void testLongAdder() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    lacount.increment();
                }
                long endtime = System.currentTimeMillis();
                System.out.println("LongAdderThread spend:" + (endtime - starttime) + "ms" + " v-" + lacount.sum());
            }).start();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LongAdderDemo demo = new LongAdderDemo();
        demo.testSync();
        demo.testAtomic();
        demo.testLongAdder();
    }
}

 

猜你喜欢

转载自blog.csdn.net/qq_39915083/article/details/107428283