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

时间:2022-04-18 22:29:15

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

?
1
int i =12;

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

1 原子性

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

int i =12;

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

1.1 java中的原子性操作

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

例如:

?
1
2
3
4
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
2
3
4
5
6
//线程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 单个线程内程序的指令重排序

例如:

?
1
2
3
4
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
2
3
4
5
6
7
8
9
//线程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关键字来保证一定的“有序性”。
当然可以通过synchronizedLock来保证有序性,很显然,synchronizedLock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

3.4 volatile 保证有序性的原理

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

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

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

4 实例分析

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 结合

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 结合

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
    }
}

到此这篇关于java并发编程之原子性、可见性、有序性 的文章就介绍到这了,更多相关java并发编程之原子性、可见性、有序性 内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.51cto.com/928343994/2841441