java并发_volatile

在并发编程中,我们通常会遇到以下三个问题:原子性问题可见性问题有序性问题。我们先看具体看一下这三个概念:

1、原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

2、可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

有缓存就会数据不一致的情况,比如:a线程从内存中读取了flag=1,放入自己的cup缓存中,此时b线程突然杀入将flag改成了2a线程完全不知道,还在使用自己cup缓存中的flag

每个线程在cup中都有自己的一块缓存:

实例:

public class TestMain {
    static Integer flag = 1;
    public static void main(String[] args) throws InterruptedException {
        Thread thread_a= new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag==1) { }
                System.out.println("thread_a  线程停止");
            }
        });
        Thread thread_b= new Thread(new Runnable() {
            @Override
            public void run() {
                try { Thread.currentThread().sleep(6); } catch (Exception e) { }
                flag = 2;
                System.out.println("flag被修改为" + flag );
            }
        });
        thread_a.start();
        thread_b.start();
         while (true) ;
    }
}

该例中,即使thread_b将flag设置为2,thread_b也无法跳出循环。 cup缓存和内存有一套同步机制, 上例中 while (flag == 1) {} 是不会触发数据同步的。我们在while循环中加入System.out.println("a"),每次循环都会触发数据同步。

                while (flag == 1) {

                       System.out.println("a");

                 }

cup缓存和内存有一套同步机制,我们的程序不好控制。有没有别的方式呢?

volatile修饰的变量具有可见性。一个线程修改了volatile修饰变量,该变量会被写回内存同时其它线程的cup缓存中的该变量会被设置成失效,在读取volatile变量时,由于变量会被置为失效,重新从主存中进行读取,这样保证了数据的同步。

public class TestMain {

    static volatile Integer flag = 1;

    public static void main(String[] args) throws InterruptedException {

        Thread thread_a = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag == 1) {
               
                }
                System.out.println("thread_a  线程停止");
            }
        });

        Thread thread_b = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.currentThread().sleep(6);
                } catch (Exception e) {
                }
                flag = 2;
                System.out.println("flag被修改为" + flag);
            }
        });

        thread_a.start();
        thread_b.start();
        while (true) ;
    }
}

volatile的使用原则

理解volatile关键字是熟悉Java并发编程的必经之路。如果要彻底理解volatile,首先需要理解Java内存模型。

volatile是java提供的一个轻量级的同步机制,用来对被修饰的变量进行同步。使用volatile修饰的变量会对多个线程可见,也就是说任何线程都可以看到被volatile修饰的变量的最终值。volatile并不能替代synchronized,因为volatile只提供了可见性,并没有提供互斥性;在多线程并发修改某个变量值时,依然会出现并发问题。所以volatile最适合用的场景是一个线程修改被volatile修饰的变量,其他多个线程获取这个变量的值。当多个线程并发修改某个变量值时,必须使用synchronized来进行互斥同步。有序性和可见性仅对volatile字段进行一次读取或更新操作起作用。声明一个引用变量为volatile,不能保证通过该引用变量访问到的非volatile变量的可见性。同理,声明一个数组变量为volatile不能确保数组内元素的可见性。volatile的特性不能在数组内传递,因为数组里的元素不能被声明为volatile。

 

3、有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

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

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

     在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。volatile关键字禁止指令重排序有两层意思:

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

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

举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

 由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

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

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

猜你喜欢

转载自blog.csdn.net/hong10086/article/details/81837954