并发编程的艺术-JAVA内存模型

并发编程的艺术-JAVA内存模型

1.JMM

1.1 什么是JMM?

1.2 可见性分析与解决

1.2.1 CAS

1.2.2 ABA

1.3 什么是原子性?

1.4 什么是连续性?

2.Volatile

2.1 什么是Volatile?

2.2 不保证原子性原因及分析

2.3 不保证原子性解决办法

1.1 什么是JMM?

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。

先看看JMM的特性:

可见性
原子性
有序性

1.2 可见性分析与解决

大家都知道,数据是存在主内存中的,线程想要操作主内存,需要先读取主内存中的数据,然后操作后再写入主内存,很简单的流程,那么为什么需要可见性呢?

我们来看这样一种情况,假如线程A和线程B同时操作主内存中的数据N,他们同时读取了N的初始数据,然后线程A操作之后将修改后的数据写入主内存,可是线程B不知道N已经被修改!!!这是很严重的错误,所以我们需要在主内存中的共享变量被修改后,通知其他正在操作这个数据的线程数据已修改,然后其他线程重新读取主内存中的最新数据N,继续操作,这个就是可见性,也是其存在的必要性。

可行性解决方案:

声明共享变量为volatile
使用CAS的原子条件更新来实现线程之间的同步
配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

1.2.1 CAS

CAS,compare and swap,比较并交换。他要求线程在操作之后,要将数据写入主内存之前,先判断主内存中的数据是不是之前的数据,如果是则写入主内存,如果不是则说明已经被其他线程修改过了,然后就会读取最新数据然后继续操作,这就是CAS。

CAS不是绝对完美的,他有自己的缺陷,比如每次要将数据写入主内存时,主内存中的数据都被其他线程修改过了,他就会一直反复回旋。另外,CAS只能保证一个共享变量的原子操作。

最主要的是,CAS会出现ABA情况。

1.2.2 ABA

什么是ABA?简单理解就是,假如线程A要操作主内存中的某个数据,然后线程A操作时间假如10秒,但是在这10秒内,有另一个线程B操作了该数据,然后又将数据还原,等线程A将数据写入主内存时,发现数据还是之前的数据,就会将数据写入内存,可这个数据已经被线程B修改了两次!!这就是ABA。

ABA解决办法,上锁,或者原子引用。

1.3 什么是原子性?

原子性:一个操作不可分割的,不可分离的。举个简单例子,对于变量x,进行加1,然后取到值,这一个过程尽管简单,但是却不具备原子性,因为我们要先读取x,之后进行计算,然后重新写入,其实是几个步骤。如果仅仅对x进行赋值,那么则可以认为是原子的,JMM是保证其原子性的。

1.4 什么是有序性?

我有一个朋友,他打字打累了,然后他决定随便写写,他说,代码里写的代码,他不是按顺序执行的,他会重新排序,然后执行,原因和细节想了解的请转去源码。有序性就是要求他不重新排序。

2 Volatile

2.1 什么是Volatile?

简单来说,Volatile就是Java虚拟机提供的一个轻量级的同步机制。
为什么说是轻量级,大家先看看Volatitle的三个特点:

可见性
不保证其原子性
禁止指令重排
可见性我在JMM章节已经讲了,禁止指令重排相当于JMM的连续性,在这里也不多叙述了,我们重点分析下不保证其原子性这个特点。

2.2 不保证原子性原因及分析

大家都知道,JMM是保证其原子性的,即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在JMM篇章我们已经说过,为了保证可见性,我们需要用到Volatile去保证其可见性,那么我们考虑下这个情况:

public class TestMain {

public static void main(String[] args) {

    TestData testData=new TestData();//有个初始值为0的变量和一个加一的方法

    //创建20个线程,每个线程执行1000次给变量加一的方法
    for(int i=0;i<20;i++){
        new Thread(()->{
            for(int j=0;j<1000;j++){
                testData.addOne();
            }
        },String.valueOf(i)).start();
    }

    //保证20个线程跑完
    while(Thread.activeCount()>2){
        Thread.yield();
    }

    System.out.println(Thread.currentThread().getName()+"最后的值为:"+testData.number);
}

}
class TestData{
volatile int number=0;
public void addOne(){
number++;
}
}

假如我们创建20个线程,每个线程执行1000次给常量加一的方法,如果保证其原子性,正常的数字应该是20000,这点大家应该没有疑惑,那么最后结果是什么呢?
image
无论你执行多少次,他最后的值永远都不会是20000,那么为什么呢?

我们根据JMM的保证可见性的前提下思考一种情况,假如两个线程同时操作主内存里的同一个变量,初始值为0,然后线程A拿到变量后,给变量加1,在他想要将值写入主内存的时候,线程B拿到了主内存中的变量,这个变量是初始值0,然后B线程拿到值之后,A线程将值写入了主内存,次数主内存中的变量值为1,然后B线程执行完操作后值也为1,然后将值写入主内存,同时写了两个1,主内存的值还是1,这就产生了丢失写值的情况。所以无论我们执行多少次,他最后的值都不可能是20000。
image
因此,Volatile不保证其原子性。

2.2 不保证原子性解决办法

解决办法有两种,一种是给方法上锁,另一种就是原子引用(atomicreference)。CAS时原子引用会出现ABA问题,简单理解就是假如A线程操作10秒,在这十秒内有另一个线程操作了数据,但又把数据还原,但是A线程发现不了,所以还是直接用时间戳原子引用(atomicstampedreference)吧。

posted @ 2021-07-10 16:03  leayun  阅读(64)  评论(1编辑  收藏  举报