Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

https://blog.csdn.net/guyuealian/article/details/52525724#commentBox
https://blog.csdn.net/SEU_Calvin/article/details/52370068

1 Java内存模型 JMM

在说明Java多线程内存可见性之前,先来简单了解一下Java内存模型(JMM)

  1. Java所有变量都存储在主内存
  2. 每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)
    在这里插入图片描述
  3. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写
  4. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

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

  • 把工作内存1中更新过的共享变量刷新到主内存中
  • 将主内存中最新的共享变量的值更新到工作内存2中
    在这里插入图片描述

2 可见性与原子性

可见性:一个线程对共享变量的修改,更够及时的被其他线程看到。
原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:
① 取出a和b
② 计算a+b
③ 将计算结果写入主内存

2.1 Synchronized:保证可见性和操作的原子性

Synchronized实现原子性和可见性的原理:在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁

2.2 Volatile:保证可见性,但不保证操作的原子性

Volatile 保证可见性的原理:Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性,例如:

    Private int Num=0;
    Num++;//Num不是原子操作

Num++不是原子操作,因为其可以分为:

  1. 从主内存中读取Num的值;
  2. 将Num的值+1,写入最新的Num的值到主内存。

对于Num++;操作,线程1和线程2都执行一次,最后输出Num的值可能是:1或者2
【解释】输出结果1的解释:当线程1执行Num++;语句时,先是读入Num的值为0,倘若此时让出CPU执行权,线程获得执行,线程2会重新从主内存中,读入Num的值还是0,然后线程2执行+1操作,最后把Num=1刷新到主内存中; 线程2执行完后,线程1由开始执行,但之前已经读取的Num的值0,所以它还是在0的基础上执行+1操作,也就是还是等于1,并刷新到主内存中。所以最终的结果是1

volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。

一般在多线程中使用volatile变量,为了安全,对变量的写入操作不能依赖当前变量的值如Num++或者Num=Num*5这些操作。

2.2.1 volatile禁止指令重排

指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性

程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成且结果已经对后面的操作可见在其后面的操作肯定还没有进行

指令重排 举例:
因为指令重排序,有可能语句2会在语句1之前执行,可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了。

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

2.3 Synchronized和Volatile的区别比较

1)Synchronized保证内存可见性和操作的原子性
2)Volatile只能保证内存可见性
3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞)synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized
4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化)
5)volatile是变量修饰符仅能用于变量,而synchronized可用于类方法或代码块的修饰符

2.3.1 必须使用synchronized而不能使用volatile的场景

例子中用new了10个线程,分别去调用1000次increase()方法,每次运行结果都不一致,都是一个小于10000的数字。自增操作不是原子操作,volatile 是不能保证原子性的。使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10000的数字。

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);
    }
}

但是使用synchronized对部分代码进行如下修改,就能保证同一时刻只有一个线程获取锁然后执行同步代码。运行结果必然是10000。

public  int inc = 0;
public synchronized void increase() {
        inc++;
}

【参考资料】《细说Java多线程之内存可见性》http://www.imooc.com/video/6775(含视频和代码)

猜你喜欢

转载自blog.csdn.net/csdnlijingran/article/details/88344969