java并发编程之原子性、可见性、有序性

在java中,执行下面这个语句

int i =12;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存(物理内存)当中。

1 原子性

定义:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

int i =12;

假若一个线程执行到这个语句时,暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

1.1 java中的原子性操作

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
例如:

int x = 10;     //语句1
int y = x;     //语句2
x++;           //语句3
x = x + 1;     //语句4

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中,所以是原子性操作。

语句2实际上包含2个操作,它先要去读取x的值,再将y的值写入主存,虽然读取x的值以及 将y的值写入主存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
语句3 语句4 同理,先将x的值读取到高速缓存中,然后+1赋值后,再写入到主存中。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

2 可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

2.1 可见性问题

例如:

//线程1
int i =12;
i=13;

//线程2 
int j=i;

假若执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i =13这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为13,那么在CPU1的高速缓存当中i的值变为13了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是12,那么就会使得j的值为12,而不是13。

这就是可见性问题,也就是说 i 的值在线程一中修改了,没有通知其他线程更新而导致的数据错乱。

2.2 解决可见性问题

Java提供了volatile关键字来保证可见性。

也就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

3 有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

3.1 单个线程内程序的指令重排序

例如:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

按照我们日常的思维,程序的执行过程是从上至下一行一行执行的,就是说按照代码的顺序来执行,那么JVM在实际中一定会这样吗??? 答案是否定的,这里可能会发生指令重排序(Instruction Reorder)。

指令重排序(Instruction Reorder) 是指: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

需要注意的是:处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

3.2 多线程内程序的指令重排序

重排序不会影响单个线程内程序执行的结果,但是多线程就不一定了。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

3.3 保证有序性的解决方法

在Java里面,可以通过volatile关键字来保证一定的“有序性”。
当然可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

3.4 volatile 保证有序性的原理

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性,也就是说:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

4 实例分析:

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

一般说来 有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。但实际中并不是这样,进行过测试后会发现,每次执行结束后,得到的都是一个比10000要小的值。

4.1 原理分析

自增操作是不具备原子性的,它包括读取变量的原始值到高速缓存中、进行加1操作、写入主存中这三个过程。
也就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,
此时 变量inc的值还没有任何改变,此时线程2拿到的值也为10,然后进行加1操作,然后将值11写入到主存中,
然后线程1继续进行加1操作 这里线程1中 inc的值依然为10,进行加1操作,然后将值11写入到主存中

那么两个线程分别进行了一次自增操作后,inc只增加了1。

4.2 synchronized 结合
public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
4.3 Lock 结合
public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
4.4 使用AtomicInteger替换int
public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
早起的年轻人 CSDN认证博客专家 移动开发 项目管理 Java
只要用心去做,每一件事情还是有可能成功的,当然成功是没有界限的,只不过是达到自己心里的那个目标,公众号:我的大前端生涯,一个爱喝茶的程序员,通常会搞搞SpringBoot 、Herbinate、Mybatiys、Android、iOS、Flutter、Vue、小程序等.
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页