内存中的原子性

160 阅读4分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

内存中的原子性

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

原子性

原子性:不可分割,操作不能打断。

我们上节在对内存可见性造成影响的代码中讲述了可以使用volatile来保证内存的可见性,当工作内存的值变化了会立即刷新到主内存中,同时使其他线程的工作内存的值失效,从而从主内存中重新拉取新的值。那么多线程同时操作一个值,仅仅只有可见性,能得到我们预期的结果吗?

写个例子来看一下

示例

package com.wangscaler.jmm;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */public class Atomicity {
    volatile int num = 0;
​
    public void add() {
        this.num++;
    }
​
    public static void main(String[] args) {
        Atomicity atomicity = new Atomicity();
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicity.add();
            }
        }, "Add").start();
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicity.add();
            }
        }, "Add2").start();
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("两个线程结束后最终的num值为" + atomicity.num);
    }
}

正常来说,我们预期的结果应该是200000。然而当我们执行的时候却总是是少于这个数的,当然也是有几率达到预期值。为什么执行的结果大出意料呢?

this.num++;的字节码如下(IDEA查看字节码,可参考往期文章从字节码讲解i++和++i的区别|8月更文挑战):

 2 getfield #2 <com/wangscaler/jmm/Atomicity.num : I>
 5 iconst_1
 6 iadd
 7 putfield #2 <com/wangscaler/jmm/Atomicity.num : I>

getfield从主内存读取num的值,iconst_1将值放到操作数栈位置一的位置,iadd进行++操作,putfield写回主内存。在多线程中,这四步中间可能会和其他线程交替执行。

  • 假设当主内存为10的时候,线程Add读取了主内存的num的值10(Add执行字节码getfield)
  • 紧接着Add2的线程也读取了主内存num的值10(Add2执行字节码getfield)
  • 然后两个线程分别进行了+1操作(Add先执行字节码iadd,随后Add2执行字节码iadd),因为工作内存的数据变化了,又分别写入主内存。
  • 假设线程Add先写入11;其后线程Add2也写入了11。(Add先执行字节码putfield,随后Add2执行字节码putfield)
  • 这时就出现了我们非预期的结果,我们预期的是经过两个线程之后,值应该为12,现在的结果却是11。
  • 多次出现这种情况那么最终的值肯定是低于200000。

如何解决原子性问题

以下的修改三次修改均是在上面示例的基础上进行修改的。

1、原子操作类(CAS)

num的类型由int修改为AtomicInteger;将add方法的num++;修改为num.getAndIncrement();如下所示:

package com.wangscaler.jmm;
​
import java.util.concurrent.atomic.AtomicInteger;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */public class Atomicity {
    AtomicInteger num = new AtomicInteger();
​
    public void add() {
        num.getAndIncrement();
    }
​
}

为什么AtomicInteger会保证原子性呢,我们打开源码发现

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

继续查看getAndAddInt的源码

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;
}

在这个getAndAddInt方法里是个循环,循环判断主存的值V和你预期的值A一样时,才会允许你将新值B写入主存,否则重新循环在主存获取值再次比较。(具体的细节可翻阅CAS,后期我会专门写什么是CAS,CAS操作包含三个操作数 :内存位置(V)、预期原值(A)和新值(B)。)

除了double、long之外的所有基本类型的读取或赋值也都是原子性操作。但是读取并赋值的操作不是原子性的例如我们常见的i++就不是原子性操作。

2、synchronized

给add方法使用关键字synchronized修饰。

public synchronized void add() {
    this.num++;
}

使用这个关键字之后,就保证了操作不能打断。也就是说一个线程执行add方法时,需要等待add的所有字节码执行完之后,下一个线程才能执行。

  • 当Add线程获得add方法的执行权之后,其他线程Add2执行到add方法时将阻塞。
  • Add线程从主内存读取num,进行++操作,写回主内存
  • Add2线程才能获得add方法的执行权。

从而保证了num的值达到我们预期的效果。synchronized就是给Add方法加了一把锁,所以我们也可以自己去实现这个锁。

3、Lock锁

package com.wangscaler.jmm;
​
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */public class Atomicity {
    volatile int num = 0;
    Lock addLock = new ReentrantLock();
​
    public void add() {
        addLock.lock();
        try {
            num++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            addLock.unlock();
        }
    }
}

和synchronized一样我们给add方法加了一把锁,这样谁获取到权限谁才能执行。

总结

在保持原子性上,优先使用原子操作类(CAS),因为他是非阻塞的同步机制的乐观锁,而synchronized、Lock两种加锁的机制是阻塞的,是一种悲观的互斥锁,大大影响我们的效率。