Java内存模型详解与可见性

JMM概念

什么是JMM

JMM(Java Memory Model) 描述多线程程序的一些合法行为,一些规则.
JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序.
如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样.
JMM是工具类和关键字的原理:

  • volatile、synchronized、Lock等的原理都是JMM
  • 如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,会非常麻烦

《深入理解Java虚拟机》一书中对JMM的讲解. 推荐大家看一下12章的内容,是我认为对Java内存模型的概念讲解的最清楚的.

Java 虚拟机规范中试图定义一种Java内存模型(Java Memory Model JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果.

Java内存模型的主要目标是:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中去除变量这样的底层细节,注意此处的变量(Variables)与Java编程中所说的变量有所区别.其实就是线程共享的区域的变量,堆内存区域:包括了实例字段、静态字段和构成数组对象的元素而线程私有的变量不存在竞争问题.

PS:注意区别:JVM 内存结构、Java内存模型、Java对象模型

  • JVM内存结构,和Java虚拟机的运行时区域有关
  • Java内存模型,和Java的并发编程有关
  • Java对象模型,和Java对象在虚拟机中的表现形式有关

如下图JVM内存结构:

在这里插入图片描述

如下图是Java对象模型:
在这里插入图片描述

如下图是Java内存模型:
在这里插入图片描述

为什么需要JMM

从Java代码到CPU指令的变化过程:
在Java代码中,使用的控制并发的手段例如synchronized关键字,最终也是要转化为CPU指令来生效的,从Java代码到最终执行的CPU指令的流程:

  1. 编写Java代码,*.java文件
  2. 在编译(javac命令)后,从刚才的*.java文件会变出一个新的Java字节码文件(.class)
  3. JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
  4. 机器指令可以直接在CPU上运行,也就是最终的程序执行

而不同的JVM实现会带来不同的"翻译",不同的CPU平台的机器指令又千差万别;

所有我们在Java代码层写的各种Lock,其实最后依赖的是JVM的具体实现(不同版本会有不同实现)和CPU指令,才能帮我们达到线程安全的效果.

由于最终效果依赖处理器,不同处理器结果不一样,这样无法保证并发安全,所以需要一个标准,让多线程运行的结果可预期,这个标准就是JMM

多线程中的问题:

  • 所见非所得
  • 无法用肉眼去检测
  • 不同的平台不同的运行结果
  • 错误很难重现

如下例子1.1 : 就是多线程问题,无法得到预期结果

public class DomeVisbility {
    int i = 0;
    boolean interrupt = true;

    public static void main(String[] args) throws InterruptedException {
        DomeVisbility domeVisbility = new DomeVisbility();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("I am ...." + domeVisbility.interrupt);
                while (domeVisbility.interrupt) {
                    domeVisbility.i++;
                }
                System.out.println(domeVisbility.i);
            }
        }).start();
        Thread.sleep(3000L);
        domeVisbility.interrupt = false;
        System.out.println("shutdown...");
    }
}

从上述的例子中,我们试着猜测以下结果:3S以后线程就会停止运行,输出i的值.
实际结果如下: 线程并没有停止还在运行的状态,这是为什么呢? 明明已经改变了interrupt为false了.
在这里插入图片描述

了解了Java内存模型后,来分析一下例子1.1 出现这个问题的原因,加深对Java内存模型的理解.
如下图所示:
主要导致的原因其实就是,主线程中修改了interrupt,在子线程中不可见. 一部分原因是CPU高速缓存在极短的时间内不可见,另外一点即使interrupt已经同步到了主内存中,但是子线程中还是没有读到interrupt,这是什么原因呢?
在这里插入图片描述

OK,不要着急,下面会讲解为什么会出现这样的问题,以及如何解决.

上述问题,也就说明为什么我们需要JMM,其实JMM就是帮助我们来解决并发安全问题的.

  • C语言不存在内存模型的概念
  • 依赖处理器,不同处理器结果不一样
  • 无法保证并发安全
  • 需要一个标准,让多线程运行的结果可预期

重排序(指令重排)

从下面几点来讲解重排序

  • 重排序的案例,什么是重排序
  • 重排序的好处:提高处理速度
  • 重排序的3种情况:编译器优化、CPU指令重排、内存的“重排序”

下面深入的去了解典型的重排序案例,更加深入理解什么是重排序.

重排序典型案例

除了在上述提到的问题是重排序的一种,还有一个典型的重排序案例,如下例子 1.2:
主要的核心在于 线程1 :a=1 x=b 线程2: b=1 y=a 在之前的线程核心基础中,可以知道两个线程的执行顺序没有办法保证的,那么就会导致x,y会有多个结果,多线程的问题. 通过 (x == ? && y == ?) 来查询会存在几种结果

/**
 * 演示重排序
 * "直到达到某个条件才停止" 测试小概率事件
 */
public class OutOfOrderExecution {

    private static int a, b = 0;

    private static int x, y = 0;

    private static int index = 0;

    public static void main(String[] args) throws InterruptedException {
        //定义闸门
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (; ; ) {
            index++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread oneThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;

                    x = b;
                }
            });
            Thread twoThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;

                    y = a;
                }
            });
            oneThread.start();
            twoThread.start();
            //放开闸门
            countDownLatch.countDown();
            oneThread.join();
            twoThread.join();
            String result = "第" + index + "次(x:" + x + " y:" + y + ")";
            if (x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

分析一下,最终会有几种结果呢? 如下一共有三种情况

  1. a = 1; x = b(0);b=1;y=1, 结果是x=0,y=1
  2. b = 1; y = a(0);a=1;x=b(1),结果是x=1 y=0
  3. b = 1; a = 1;x=b(1) y=a(1),结果是x=1 y=1

运行代码 之后会发现还存在一种情况就是x=0 y=0? 会为什么会出现这种情况呢?
这种情况是极小概率出现的,当运行到6万多次的时候才出现了. 出现这个问题的原因:就是代码的执行顺序发生了改变,也就是发生了重排序

第259次(x:0 y:1)
第260次(x:1 y:1)

第328次(x:1 y:0)

第60926次(x:0 y:0)

代码的执行顺序只有一种可能:

y=a;
a=1;
x=b;
b=1;

什么是重排序

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y=a和b=1这两行语句.

如下图所示: 重排序将代码的执行顺序改变了,导致了出现我们认为不可能出现的情况.

在这里插入图片描述

重排序的好处

既然重排序会带来线程的安全问题,为什么还要使用从排序呢?

重排序会提高处理速度.

如下图所示,来看CPU指令重排是如何提高处理速度的:
如果按照正常的指令运行,会对a 有一个重复load 和 store的过程,很显然这个过程是可以进行优化的.
CPU 指令重排序后,将对变量a的操作进行了优化,并且减少了一次 load a 和 store a的操作.很大程度上提高了性能.这也正是CPU指令重排带来的好处.
在这里插入图片描述

重排序的三种情况:

    1. 编译器优化:包括JVM、JIT编译器等
    1. CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
    1. 内存的“重排序”:线程A的修改,线程B却看不到,这是可见性问题

下面再来看,重排序对编译器优化 JIT编译器.在开篇的时候,给出了例子1.1 ,例子中出现的原因是什么呢? 例子中只有一个变量不能像例子1.2导致的问题一样,例子1.2的情况其实就是第二种重排序的情况,那么例子1.1其实就是第一种的重排序情况,也就是编译器优化导致的.
**
JIT编译器(Java In Time Compiler).在这之前需要先了解脚本语言编译语言的区别.
解释执行:在执行时,有其语言的解释器将其一条一条的翻译成机器可识别的指令.
编译执行:将编写好的代码,直接编译成机器可识别的指令.

Java 是属于脚本语言还是编译语言呢?

如果你说Java是编译语言那是错误的,其实Java是介于脚本语言和编译语言之间的,Java都有脚本语言和编译语言的特点.
为什么呢? 下面我来带你揭开这层面纱

如下图所示: 通过下图来分析例子1.1 中的代码是如何执行的.
首先会将例子1.1中的代码编译为字节码, 执行前的编译器不会导致指令重排,指令重排发生在JIT编译器中. 然后这些字节码会在JVM进程中跑,执行这些字节码,有解释执行和编译执行,** 首先会进行解释执行,解释执行就是一条条的翻译成机器指令,比如: 执行“read interrupt” 然后再执行 “判断interrupt” … 进行执行, 但是有一个问题,在while循环体中,不停的执行相同的字节码,而解释执行会导致效率很低,每次都会对指令进行编译. 当一个方法被调用多次或者方法中的循环体循环了多次就进入编译执行,将其进行整体的编译,编译之后缓存到方法区.** (方法区会存:静态字段 类信息 JIT编译之后的代码) JIT编译器会对其整体编译后的字节码进行优化,如下图中的JIT部分. 这也是例子1.1中,while的循环没有被终止的原因.
也就是说,例子1.1中在开始的时候是进行解释执行的,但是while循环了很多次,JVM就是对其进行优化,将while循环整体进行编译然后JIT又对其进行了优化将interrupt缓存到了方法区,这也就导致了每次从方法去缓存中读取到的interrupt=true,while循环不能被终止,最终导致了例子1.1 不能得到预期的结果.
在这里插入图片描述

除了上述重排序情况1(JVM和JIT的优化)主要原因 还有情况3(内存的“重排序”:线程A的修改,线程B却看不到,这是可见性问题) 这两个原因导致例子1.1 不能够结束循环.进一步引入了可见性的问题.
我们再来看这个图,结合上图就可以分析出例子1.1 while循环不能够被终止的原因是:** CPU高速缓存在极短的时间内不可见的,一段时间后还是会同步到主内存中,但是while是一个循环不停的从主内存中获取interrupt的值,每次都是true这是因为JVM和JIT优化导致的,方法体中的循环被多次执行,JIT将循环体中缓存到了方法区,每次运行直接从方法区中读取缓存,而方法区缓存的interrupt=true,导致while循环不能被终止.其实就是主线程写和子线程读的原因.**
在这里插入图片描述

经过重重分析,我们终于找到了例子1.1 问题的根本原因,是不是有一种非常畅快的感觉.
如何解决例子1.1中的问题呢? 通过volatile关键字即可解决

volatile boolean interrupt = true;

运行代码:while已经被正确的停止,并且打印出了i的值.

I am …true
shutdown…
1968556129

为什么volatile关键可以解决这个问题呢? volatile如何实现它的语义的呢?

  • 禁止缓存:volatile变量的访问控制符会加上ACC_VOLATILE
  • 对volatile变量相关的指令不做重排序

JVM 的规范中,文档对ACC_VOLATILE的解释:Declared volatile;cannot be cached.(不能够被缓存)
在这里插入图片描述

我们来反编译一下例子1.1 的.class文件:javap -v -p DomeVisbility.class 之后会看到如下class字节码,将interrupt的flags:ACC_VOLATILE.

.....  
volatile boolean interrupt;
    descriptor: Z
    flags: ACC_VOLATILE
 .....

通过上述,volatile 描述的变量,是禁止缓存和禁止重排序的,这样就解决了例子1.1中的:CPU高速缓存就不能够被缓存 以及 JIT 就不允许进行编译优化,这样就不会出现重排序和缓存到方法区的问题.
接下来具体展开对可见性的深入讲解.

可见性

接下来我会通过,如下步骤来深入展开对可见性的讲解

  1. 演示什么是可见性问题
  2. 为什么会有可见性问题
  3. JMM的抽象:主内存和本地内存
  4. Happens-Before原则
  5. volatile关键字
  6. 能保证可见性的措施
  7. 升华:对synchronized可见性的正确理解

演示可见性问题

例子1.3 如下代码: 一个线程修改数据,一个线程读取数据

/**
 * 演示可见性带来的问题
 */
public class FieldVisibility {
    int a = 1;
    
    int b = 2;

    public static void main(String[] args) {
        while (true) {
            FieldVisibility fieldVisibility = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    fieldVisibility.change();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    fieldVisibility.print();
                }
            }).start();
        }
    }

    private void print() {
        System.out.println("b=" + b + " a=" + a);
        //b=3 a=3
        //b=2 a=1
        //b=2 a=3
        //可见性问题
        //b=3 a=1
    }

    private void change() {
        a = 3;
        b = a;
    }
}

其实上述代码和例子1.2代码类似,预期的结果一个有三种,但实际是有四种结果

b=3 a=3
b=2 a=3
b=2 a=1
//可见性问题
b=3 a=1

原因分析:为什么a已经赋值3了,获取到的a=1呢? 两个线程看到的变量的值可能是不一样的,假设线程1 把a=3 b=3,但是线程2有可能出现只看到了一部分,两个线程通信都是通过主存,通信是有延时和代价的,在这种情况很可能只看到了b而没有看到a,所以线程2看到的是初始化时候的a.
类似下图所示: reader-thread读到的值很可能不是x=1,writer-thread写入的值还没有同步到主内存 这时候reader-thread获取到的值还是默认的初始化的值
在这里插入图片描述

解决上述问题只需要加上volatile即可

volatile int a = 1;
volatile int b = 2;

如下图所示:加上volatile后会强制(flush)将a和b的值刷入到主内存中,线程2就可以读取到修改后的值
每个线程都有自己的工作内存,而可能存在writer-thread到写入操作,还没有同步到主内存,reader-thread 会从主内存中读取.
volatile 会将写入操作,强制刷入到主内存中,所以就可以读取到x=1
在这里插入图片描述

在这里插入图片描述

为什么会有可见性问题

CPU有多级缓存,导致读的数据过期

  • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层.
  • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的.
  • 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中,所以会导致有些核心读取的值是一个过期的值

如下图所示:
在这里插入图片描述

JMM 的抽象:主内存和本地内存

  • Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不在需要关心以及缓存和二级缓存的问题,但是JMM抽象了主内存和本地内存的概念
  • 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象

如下图所示:
在这里插入图片描述

主内存和本地内存的关系

JMM有一下规定:

  • 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
  • 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后在同步到主内存中
  • 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转换来完成

所有但共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题.

Shared Variables定义

Memory that can be shared between threads is called shared memory or heap memory.
All instance fields, static fields, and array elements are stored in heap memory. In this chapter, we use the term variable to refer to both fields and array elements.
Local variables (§14.4), formal method parameters (§8.4.1), and exception handler parameters (§14.20) are never shared between threads and are unaffected by the memory model.
Two accesses to (reads of or writes to) the same variable are said to be conflicting if at least one of the accesses is a write.

可以在线程之间共享的内存称为共享内存或堆内存.
所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数据都是标题中提到的共享变量.
冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的.
这些能被多个线程访问的共享变量是内存模型规范的对象.

线程间操作

注意:所有线程间操作,都存在可见性问题,JMM对其进行了规范

  1. 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响
  2. Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行.

线程间操作:

read操作 (一般读,即 非volatile读)
write操作(一般写,即 非volatile写)
volatile read
volatile write
Lock. (锁monitor)、Unlock
线程的第一个和最后一个操作
外部操作

同步规则的定义如下:

也就是遵循如下的同步规则,不会发生线程安全问题可见性问题

  1. 对volatile变量v的写入,与所有其他线程后续对v的读同步(使用volatile修饰的变量)
  2. 对于监视器m的解锁与所有后续操作对于m的加锁同步(也就是synchronized lock)
  3. 对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同
  4. 启动线程的操作与线程中的第一个操作同步(例如在线程2启动线程1 线程1的状态Runnable是同步的)
  5. 线程T2的最后操作与线程T1发现线程T2已经结束同步(isAlive join可以判断线程是否终结,就是线程的终止状态是同步的)
  6. 如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中了同步,通过抛出InterruptedException异常,或者调用Thread.interrupted或thread.inInterrupted(也就是线程终止状态同步)

happens-before 原则

  • happens-before 原则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before
  • 两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的.
  • 当程序包含两个没有被happens-before关系排序的冲突访问时,就存在数据竞争,遵循了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存.

那么什么不是happens-before呢?

两个线程没有互相配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就不具备happens-before

Happens-Before 的规则有哪些?

  1. 单线程规则

happens-before 并不影响重排序

  1. 锁操作(synchronized lock)

在这里插入图片描述

  1. volatile
  2. 线程启动

在这里插入图片描述

  1. 线程join

在这里插入图片描述

  1. 传递性:如果hb(A,B)而且hb(B,C),那么可以推出hb(A,C)
  2. 中断:一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到
  3. 构造方法:对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令
  4. 工具类的Happens-Before原则
  5. 线程安全的容器,get一定能看到在此之前的put等存入操作
  6. CountDownLatch
  7. Smaphore(信号量)
  8. Future
  9. 线程池
  10. CyclicBarrier

演示happens-before

还是使用上述例子,我们不用全部给a和b加上volatile,只需要给b加上volatile即可,为什么呢? 近朱者赤给b加了volatile,不仅b被影响,也可以实现轻量级的同步,b之前的写入(b=a)对读取b后的代码都可见,a即使不加volatile,只要b读到是3,就可以由happens-before原则保证了读取到的都是3而不能读取到1.

    int a = 1;
    /**
     * volatile 可以保证在b之前的所有操作,都是保证可见的
     */
    volatile int b = 2;

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

volatile 关键字

volatile 是什么?

  • volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
  • 如果一个变量被修饰成volatile,那么jvm就知道了这个变量可能会被并发修改.
  • 开销小,那么响应的能力也小,虽然volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用

volatile 适用场景

  • volatile 不适用a++

如下代码: 在之前文章“线程核心基础2”中有讲到这个问题,我们分析了数据丢失的原因,不记得的可以回去再看一看

/**
 * a++ 不适用于volatile的场景
 */
public class NoVolatile implements Runnable {

    volatile int a;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile noVolatile = new NoVolatile();
        Thread thread1 = new Thread(noVolatile);

        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(noVolatile.a);
    }
}

运行后的结果如下: 并不是我们想要的预期的结果:20000

16003

  • 适用场合1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替.

其实我们在例子1.1 中已经证明了boolean flag是适用于volatile的.

  • volatile不适合场景2

但是boolean 即使加了volatile关键字也可能出现不符合预期的情况,直接看代码进行演示:
如下代码中,开启两个线程,并且done = !done

/**
 * boolean 不适用于volatile的场景
 */
public class NoVolatile2 implements Runnable {

    volatile boolean done = false;

    AtomicInteger relaA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            done = !done;
            relaA.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile2 noVolatile = new NoVolatile2();
        Thread thread1 = new Thread(noVolatile);

        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(noVolatile.done + ":" + noVolatile.relaA.get());
    }
}

获取到两种结果: 并不符合预期:done = false

true:20000
false:20000

  • volatile 适用场合2:作为刷新之前变量的触发器

直接上代码: 触发器就是充当,之前的操作都是被其他线程可见的,在如下代码中让b来充当触发器,当线程2读到b=0的时候,那么线程1的修改肯定是对线程2可见的,这种操作在实际开发中经常使用.需要深入的理解一下

    int a = 1;
    /**
     * volatile 可以保证在b之前的所有操作,都是保证可见的 充当触发器的作用
     */
    volatile int b = 2;

    int abc = 1;
    int abcd = 1;

    private void change() {
        abc = 7;
        abcd = 70;
        a = 3;
        b = 0;
    }

    private void print() {
        if (b == 0) {
            //当b = 0当时候,可以确保b之前的所有操作都是可见的
            System.out.println("b=" + b + " a=" + a);
        }
    }

Volatile的作用

其实在上述中,已经讲过volatile的作用,这里我在重新申述一遍

  1. 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存中
  2. 禁止指令重排序优化:解决单例双重锁乱序问题

volatile和synchronized的关系?

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的.而volatile又保证了可见性,所以就足以保证线程安全

(后续章节会详细讲解synchronized)

小结

  1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步.
  2. volatile属性的读写操作都是无锁,它不能代替synchronized,因为它没有提供原子性和互斥性.因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是成本低的.
  3. volatile只作用于属性,用volatile修饰属性,这样compilers就不会对这个属性做指令重排序
  4. volatile提供了可见性,任何一个线程对其修改将立马对其他线程可见.volatile属性不会被线程缓存,始终从主存中读取
  5. volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作
  6. volatile可以使得long和double的赋值是原子的,后续文章会讲long和double的原子性.
  7. 除了volatile可以让变量保证可见性外,synchronized lock 并发集合 Thread.join() Thread.start()等都可以保证可见性
  8. happens-before原则
  9. synchronized不仅保证了原子性,还保证了可见性(后续章节会进行讲解)
原创文章 4 获赞 20 访问量 7万+

猜你喜欢

转载自blog.csdn.net/su19921021/article/details/105858750
今日推荐