浅谈多线程之volatile关键字

在介绍volatile之前,先来说说一下几个相关的概念

一、相关概念解释

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

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

重排序代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能儿做的优化

1.编译器优化的重排序(编译器优化)

2.指令级并行重排序(处理器优化)

3.内存系统的重排序(处理器优化)

如:代码的顺序为

int number =1;

int result =0;

而实际执行顺序可能为:

int result = 0;

int number = 1;

Java内存模型:Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JCM中将变量存储到内存和从内存中读取变量这样的底层细节。

如图所示:共享变量x,所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

两条规定:

1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写

2)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

共享变量可见性实现的原理

线程1对共享变量的修改要被线程2及时看到,必须要经过如下2个步骤:

1.把工作内存1中更新过的共享变量刷新到主内存中

2.将主内存中最新的共享变量的值更新到工作内存2中

二、Volatile关键字

首先被volatile修饰的变量能够保证可见性,但不能保证变量复合操作的原子性。

volatile是如何实现可见性的:

深入来说:通过加入内存屏幕和禁止重排序优化来实现的

   对volatile变量执行写操作时,会在写操作后加入一条store屏幕指令。对volatile变量执行对操作时,会在读操作前加入一条load屏幕指令。

通俗地讲:volatie变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的变量总能看到该变量的最新值。

线程写volatile变量的过程:

1.改变线程工作内存中volatile变量副本的值

2.将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的过程:

1.从主内存中读取volatile变量的最新值到线程的工作内存中

2.从工作内存中读取volatile变量的副本

volatile不能保证volatile变量复核操作的原子性:

如:private int number = 0;  number ++; 不是原子操作。number++其实可以分解为3个操作步骤:

1)读取number的值

2)将number的值加1

3)写入最新的number的值

package com.imooc.demo.hello;


public class VolatileDemo {

    // 被volatile修饰的变量
    private volatile int number = 0;
    public int getNumber(){
        return this.number;
    }
    public void increase(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.number++;
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }

        //如果还有子线程在运行,主线程就让出CPU资源,
        //直到所有的子线程都运行完了,主线程再继续往下执行
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println("number : " + volDemo.getNumber());
    }

}

运行结果:

如果程序正常执行的话,number的值应该为500,而在某个时刻,number的值却为493。这是为什么呢?

我们不妨假设初始时number=5;现在有A、B两个线程,而实际它们的运行过程可能是这样的:

1)线程A读取number的值

2)线程B读取number的值

3)线程B执行加1操作

4)线程B写入最新的number值

5)线程A执行加1操作

6)线程A写入最新的number值

预期的话,两次number=7,而如果按照上述步骤执行,两次number++只增加了1,这也就是为什么程序执行结束后number的值为493了,可见volatile关键字并不能保证复合操作的原子性。

那么有哪些方式可以保证原子性操作呢?

1)使用synchronize关键字

2)使用ReetrantLock(java.util.concurrent.locks包下)

3)使用AtomicInterger (java.util.concurrent.atomic包下)

这并不是说volatile关键字没有用了,哪些场合适合使用volatile关键字呢?

volatile使用场合:

要在多线程中安全的使用volatile变量,必须同时满足:

1.对变量的希尔操作不依赖当前值

     不满足:number++、count = count*5等

     满足:boolean变量、记录温度变化的变量等

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

     不满足:不变式 low<up

synchronized和volatile比较

1.volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;

2.从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁

3.synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子

猜你喜欢

转载自blog.csdn.net/lovebaby1689/article/details/105180765
今日推荐