java中volatile关键字的作用

java中volatile关键字的作用

  • 一、volatile是什么?
  • 二、问题及解决
    • 1、LOCK#锁的方式
    • 2、通过缓存一致性协议
  • 三、并发编程中的三个概念
    • 1、原子性
    • 2、可见性
    • 3、有序性
  • 四、案例
    • 1、可见性
    • 2、volatile的正确使用方式


一、volatile是什么?

每条指令在CPU执行的过程中,由于由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
  
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

二、问题及解决

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

i = i + 1;

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

1、LOCK#锁的方式

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

2、通过缓存一致性协议

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

三、并发编程中的三个概念

1、原子性

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

2、可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
例如:线程1对变量n修改了之后,线程2没有立即看到线程1修改的值。

3、有序性

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

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

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

四、案例

1、可见性

NonVolatileDemo中,停止标识stop未使用volatile关键字修饰,初始值为false。

  • 创建子线程thread1并启动,在子线程thread1任务中使用停滞标识stop作为判断条件:
  • 当不满足停止条件时,线程会一直运行;
  • 当满足停止条件,终止任务。
  • 稍后,我们在主线程中设置停止标识为true。
  • 我们希望在设置stop=true之后,子线程能够获得到判断条件的变化而停下来

但是执行代码,结果如下图,我们可以看到在主线程设置stop=true后,子线程未及时感知到stop的变化,还在继续执行任务。

class ThreadLocalTest3 {
    public static boolean stop = false;//任务是否停止,普通变量
    //public static volatile boolean stop = false;//任务是否停止,volatile变量

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(() -> {
            while (!stop) { //stop=false,不满足停止条件,继续执行
                //do someting
            }
            System.out.println("stop=true,满足停止条件。" +
                    "停止时间:" + System.currentTimeMillis());
        });
        thread1.start();

        Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
        stop = true; //true
        System.out.println("主线程设置停止标识 stop=true。" +
                "设置时间:" + System.currentTimeMillis());
    }
}

java中volatile关键字的作用_第1张图片
​ 在VolatileDemo中,停止标识stop使用volatile关键字修饰,初始值为false。其他代码和NonVolatileDemo完全一致。
java中volatile关键字的作用_第2张图片
初步了解了volatile的基本使用及其可见性,下面我们来看看volatile和synchronized的区别

public class SychronizedDemo {
    public static boolean stop = false;//任务是否停止

    //同步静态方法,设置stop
    public static synchronized void setStop(boolean flag) {
        SychronizedDemo.stop = flag;
    }

    //同步静态方法,获取stop
    public static synchronized boolean isStop() {
        return stop;
    }

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(() -> {
            while (!isStop()) { //stop=false,不满足停止条件,继续执行
                //do someting
            }
            System.out.println("stop=true,满足停止条件。" +
                    "停止时间:" + System.currentTimeMillis());
        });
        thread1.start();

        Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
        setStop(true); //true
        System.out.println("主线程设置停止标识 stop=true。" +
                "设置时间:" + System.currentTimeMillis());
    }
}

在SychronizedDemo中,停止标识stop为普通静态变量,初始值为false。stop的设置方法setStop或获取方法isStop都为同步方法,可以保证锁对象SychronizedDemo类静态变量stop的可见性。执行代码,结果如下图,我们可以看到在主线程设置stop=true后,子线程同时感知到stop的变化终止了任务。
java中volatile关键字的作用_第3张图片
volatile关键字是Java提供的最轻量级的同步机制,为字段的访问提供了一种免锁机制,使用它不会引起线程的切换及调度。这时使用volatile要比synchronized要简单有效的多,如果使用synchronized还会影响系统的吞吐量。

事实上,volatile关键字并不是万能的,因为我们前文讲到了volatile并不能保证原子性。

2、volatile的正确使用方式

这样只要任何一个线程调用了shutdown(),其他线程在执行doWork时都可以立即感知到stop变量的变化,这时就可以大胆的使用volatile。这种类型的状态标记的一个公共特性是:通常只有一种状态转换,如标志从false 转换为true。

volatile boolean stop=false;//volatile 变量,用于停止请求的状态标识
public void shutdown() {//停止请求
    stop = true;   
}  
public void doWork() {
    while (!stop) {//判断是否需要停止
        // do Something  
    }  
}

你可能感兴趣的:(java常规,jar,linux,服务器,多线程)