volatile 的用法和多线程运用

在博问中遇到一个多线程的问题:提问者问到volatile 

然后发现一个多线程解释非常好的博客,下面链接

https://www.cnblogs.com/dolphin0520/p/3920373.html

博客理解:

我们在编写代码的时候有时候会用到多线程:

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题

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

 简单理解就是事件要么全执行要么全不执行

2.可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

简单理解多个线程之间要相互看到对方的操作,保证自己对同一个数据操作不会受到影响

3.有序性:即程序执行的顺序按照代码的先后顺序执行。

代码是按一定顺序排列的,然而有时会发生指令重排序

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

注意:处理器在进行重排序时是会考虑指令之间的数据依赖性

单线程排序:处理器在进行重排序时是会考虑指令之间的数据依赖性,单线程不会代码顺序执行出现错误问题

多线程:

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
   上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,
而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。   从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。   也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确

内存模型

1.原子性:

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

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

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2.可见性:

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

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

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性:

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

  在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,
很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。   另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens
-before 原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。   下面就来具体介绍下happens-before原则(先行发生原则): 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始   这8条原则摘自《深入理解Java虚拟机》。

下面谈一下:volatile 按顺序理解以下几点:

1.(多线程中运行)在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的------要记住每一个线程都有自己的域,即是自己的高速缓存

2.volatile 使用作用:

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

3.volatile使用注意

通常来说,使用volatile必须具备以下2个条件:

  1)对变量的写操作不依赖于当前值

  2)该变量没有包含在具有其他变量的不变式中

  实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

  事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行

博问提问:下面第二段代码不执行步骤A ,而

static int initVal = 0; 改成 static volatile int initVal=0就可访问到A步骤
为什么?
当加入 volatile 时,新值对其他线程来说是立即可见的。A步骤被执行

当不加volatile 时,新值不是立即可见,initVal改变,另一个线程还是用老值(自己缓存线程的值),还有一种解决让另一个线程休眠一秒
如在
  new Thread(() -> {
            
            int localVal = initVal;
            while (localVal < MAX) {
                
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                
                if (initVal != localVal) {
                    b++;
                    System.out.printf("The iniVal is updated to [%d]
", initVal);// A步骤
                    localVal = initVal;
                }
            }
        }, "Reader").start();

public class VolatileDemo2 {
    final static int MAX = 5;
    static int initVal = 0;
    static volatile int b=0;
    public static void main(String[] args) {
         new Thread(() -> {
             int localVal = initVal;
             while (localVal < MAX) {
                 System.out.printf("The initVal will be cahnged to [%d]
", ++localVal);
                 initVal = localVal;

                 try {
                     TimeUnit.SECONDS.sleep(2);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 
             }
         }, "Updater").start();
         
        new Thread(() -> {
            
            int localVal = initVal;
            while (localVal < MAX) {
                if (initVal != localVal) {
                    b++;
                    System.out.printf("The iniVal is updated to [%d]
", initVal);// A步骤
                    localVal = initVal;
                }
               
            }
        }, "Reader").start();

       
    }
    
}
原文地址:https://www.cnblogs.com/jsbk/p/10120317.html