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