Java面试内容精讲—并发编程volatile

前言

观看链接:https://www.bilibili.com/video/BV1RC4y1h7YU

课程总体说明

介绍的是有关于并发编程下的volatile关键字的使用;
并发编程一直是java实际开发当中一个非常重要的内容,同时也是一个难以理解的内容;
在实际开发中或者是在面试中经常被面试官问到关于并发编程的知识点;volatile就是其中一个比较热门的知识点,也是相对于来说比较难以理解的知识点;

在介绍volatile关键字之前,先来了解一下并发编程的背景;
并发编程是在互联网的背景下,当我们的系统开发出来以后,肯定会涉及到非常多的并发量;
那么在这么大的并发量的情况下,我们的业务以及共享数据都会出现一个并发安全性的问题。
所以并发编程会衍生出非常多的技术;
在介绍volatile模块之前,介绍下本次课程的总体介绍。

并发编程-volatile使用精讲

  • 第一章 volatile关键字概览(介绍volatile是干嘛用的)
    • 多线程下变量的不可见性
      • 概述
        • 在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值。而通过volatile关键字就可以让共享变量值可见。
      • 案例演示
      • 执行结果
      • 小结
    • 变量不可见性内存语义
      • 概述
      • 问题分析
      • 小结
    • 变量不可见性解决方案
      • 概述
      • 解决方案
      • 小结
  • 第二章 volatile的其他特性
    • volatile特性概述
      • volatile总体概述
    • volatile不保证原子性
      • 问题案例演示
      • 问题原理说明
      • volatile原子性测试
      • 小结
      • 问题解决
        • 使用锁机制
        • 原子类
          • AtomicInteger
          • 案例改造
    • 禁止指令重排序
      • 概述
      • 重排序的好处
      • 重排序案例演示
      • volatile禁止重排序
      • 小结
  • 第三章 volatile内存语义
    • volatile写读建立的happens-before关系
      • 概述
      • happens-before规则
      • volatile写读建立的happens-before关系
      • volatile重排序规则小结
  • 第四章 volatile高频面试与总结
    • long和double的原子性
      • 概述
      • 案例演示
      • 测试结果
      • 小结
    • volatile在双重检查加锁的单例中的应用
      • 单例概述
      • 单例模式有8中
      • 饿汉单例的2种写法
        • 饿汉式(静态常量)
        • 饿汉式(静态代码块)
      • 懒汉式单例4中写法
        • 懒汉式(线程不安全)
        • 懒汉式(线程安全)
        • 懒汉式(线程不安全)
        • 懒汉式(volatile双重检查模式 推荐)
      • 静态内部类单例模式
      • 枚举实现单例
    • volatile的使用场景
      • 纯赋值操作
        • 概述
        • 案例代码
        • 小结
      • 触发器
    • volatile与synchronized
      • 区别
    • volatile的总结
      • 总体总结

多线程下变量的不可见性现象

多线程下变量的不可见性

概述

在多线程并发执行下(多条线程执行的情况下),多个线程修改共享的成员变量(共享的成员变量:比如说实例成员变量、一个类中的静态成员变量,那么这些都是可以被多个线程所共享的成员变量),会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值。

当使用多条线程来访问一个类当中的成员变量的时候,如果其中有一个线程将该成员变量的值给进行修改了则那么另外一个线程可能不能够马上看到这个变量的最新值。
这样就可能在并发编程的情况下,可能出现一个变量数据不同步的问题。

案例演示

当有一个成员变量的情况下,如果开了多条线程,其中某一个线程A修改了这个变量的取值以后,其他的线程B、C、D可能是无法观看得到线程A所针对这个变量所做的操作;
那么通过volatile关键字,就可以使得这个变量的取值在线程之间可见;

public class MyThread extends Thread{

  // 定义成员变量
  private boolean flag = false;
  public boolean isFlag(){ return flag; }

  @Override
  public void run(){
    try{
      Thread.sleep(1000);
    }catch(InterruptedException e){
      e.printStackTrace();
    }


    // 将flag的值更改为false
    this.flag = true;
    System.out.println("flag = " + flag)
  }
}

public class VolatileThreadDemo{
  //测试类


}
package com.xxx.concurrent;
/**
      目标:研究一下多线程下变量访问的不可见性现象

      准备内容:
        1. 准备两个线程
        2. 定义一个成员变量
        3. 开启两个线程,其中一个线程负责修改,另外一个负责读取
 */
public class VisibilityDemo01{

  // main()方法本身就是一个主线程,作为一个主线程
  public static void main(String[] args){

    // a. 开启一个子线程
    MyThread t = new MyThread();
    // 启动子线程;在子线程当中将成员变量flag修改为true
    t.start();

    // b. 主线程执行;定义成一个死循环;
    while(true){
      /**
        调用isFla取出MyThread类当中flag成员变量的值。
        创建好一个子线程t,MyThread当中的flag成员变量属于线程对象t;
        触发启动子线程之后,子线程执行完其run()方法之后,将flag的取值修改为了true;
        从而接着主线程执行判断t.isFlag()时应该取到的值为true;
        取到正确的值之后进入到if判断中来进行打印输出;

        将子线程启动的时候,应该要将flag变量修改为true;
        那么当主线程在执行的时候就会发现flag变量已经为true,
        那么就会进入到if块当中的代码进行打印输出
      */
      if(t.isFlag()){
        System.out.println("主线程进入循环执行~~~~~~");
      }
    }
  }

//单独定义一个类,子线程
class MyThread extends Thread{
  //成员变量
  private boolean flag = false;

  //重写run方法
  @Override
  public void run(){

    // 触发修改共享成员变量
    flag = true;

    //此时已经在子线程当中将共享变量flag的取值修改为true;
    System.out.println("flag = " + flag);
  }

  public boolean isFlag(){
    return flag;
  }
  public void setFlag(boolean flag){
    this.flag=flag;
  }
}

}
------------------------------------------
运行成功;将主线程当中打印内容while(true)循环输出;
flag = true
主线程进入循环执行~~~~~~
主线程进入循环执行~~~~~~
主线程进入循环执行~~~~~~
主线程进入循环执行~~~~~~
主线程进入循环执行~~~~~~
主线程进入循环执行~~~~~~
....
------------------------------------------
原因:
原因在于子线程B很快的将线程MyThread当中的flag修改成了true;
所以当主线程再来进行执行的时候,flag的取值已经为true了,所以主线程会进入到if判断中执行代码块中的打印输出;
------------------------------------------
public class MyThread extends Thread{

  // 定义成员变量(实例成员变量)
  private boolean flag = false;
  public boolean isFlag(){ return flag; }

  @Override
  public void run(){
    try{
      Thread.sleep(1000);
    }catch(InterruptedException e){
      e.printStackTrace();
    }


    // 将flag的值更改为false
    this.flag = true;
    System.out.println("flag = " + flag)
  }
}

public class VolatileThreadDemo{
  //测试类


}
/**
      目标:研究一下多线程下变量访问的不可见性现象

      准备内容:
        1. 准备两个线程
        2. 定义一个成员变量
        3. 开启两个线程,其中一个线程负责修改,另外一个负责读取
 */
public class VisibilityDemo01{

  // main()方法本身就是一个线,程
  public static void main(String[] args){

    // a. 开启一个子线程
    MyThread t = new MyThread();
    t.start();

    // b. 主线程执行
    while(true){
      if(t.isFlag()){
        System.out.println("主线程进入循环执行~~~~~~");
      }
    }
  }

class MyThread extends Thread{
  //成员变量
  private boolean flag = false;

  @Override
  public void run(){

    /**
    在实际开发过程中,假设更改flag变量取值为true修改得慢一点;
    即让子线程休眠一秒钟;
    而当中子线程在进行休眠的该过程当中主线程其实也是有机会优先执行while(true)循环体中的if判断层的;
    */
    try{
      Thread.sleep(1000);
    }catch(InterruptedException e){
      e.printStackTrace();
    }

    // 触发修改共享成员变量
    flag = true;
    System.out.println("flag = " + flag);
  }

  public boolean isFlag(){
    return flag;
  }
  public void setFlag(boolean flag){
    this.flag=flag;
  }
}

}
------------------------------------------
运行成功;将主线程当中打印内容while(true)并没有进行循环输出;
flag = true
------------------------------------------
原因:当子线程执行run()方法的时候,已经将flag变为了true;
也就是说子线程已经修改了这个变量的取值;
但是在主线程当中是没有访问得到这个变量的最新取值的;
这样也就出现了多线程下变量访问的不可见性现象。
....

执行结果

此时可以看到,子线程中已经将flag设置为true,
但是main()方法当中始终没有读取到修改后的最新值,
从而循环没有能进入到if语句中执行,所以没有任何打印。

小结

多线程下修改共享变量会出现变量修改值后的不可见性。

(当其中一个变量修改了,而其他线程是可能无法看到修改后的变量的新的取值的)

多线程下变量不可见性内存语义

概述

JMM内存模型不同于java基础当中学到的栈、堆,它是属于并发编程下的一个独有的内存模型。通过了解内存模型可以深入理解并发编程当中的底层运转机制。同时也可以更加深入理解volatile关键字,在并发编程当中的作用,以及它可以帮助我们在实际开发当中解决问题。

在连接多线程并发修改变量不可见现象前,我们需要了解回顾一下Java内存模型(和Java并发编程有关的模型):JMM。

JMM(Java Memory Model):Java内存模型是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

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

JMM有以下规定:

  • 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
  • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  • 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存

本地内存和主内存的关系:

问题分析

小结

多线程下变量不可见性解决方案-加锁,volatile修饰

概述

如何实现在多线程下访问共享变量,实现一个线程修改变量后,对其他线程可见呢。
接下来为大家介绍两种方案:

第一种加锁;
第二种是使用volatile关键字;

解决方案

加锁

//main方法
while(true){
  synchronized(t){
    if(t.isFlag()){
      System.out.println("执行了==============");
    }
  }
}

某一个线程进入synchronized代码块前后,执行过程如下:

a. 线程获得锁
b. 清空工作内存
c. 从主内存拷贝共享变量最新的值到工作内存成为副本

小结

volatile不保证原子性

第二章 volatile的其他特性

volatile特性概述

在上节中,已经研究完了 volatile可以实现并发下共享变量的可见性;
除了volatile可以保证可见性外,
volatile还具备如下一些突出的特性:
volatile的原子性问题:volatile不能保证原子性操作;
禁止指令重排序:volatile可以防止指令重排序操作;

volatile不保证原子性2

在保证线程安全的情况下,volatile是否可以做到保证原子性?
在之前是使用synchronized关键字进行加锁机制来进行保证变量修改的安全性;
而volatile无法保证线程安全性;

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

问题案例演示
public class VolatileAtomicThread implements Runnable{

  //定义一个int类型的遍历
  private int count = 0;

  @Override
  public void run(){
    // 对该变量进行++操作,100次
    for( int i = x; x<100; x++){
      count++;
      System.out.println("count ==========>>>>>>> "+count);
    }
  }
}

public class VolatileAtomicThreadDemo{
  public static void main(String[] args){

    //创建VolatileAtomicThread对象
    VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread();

    //开启100个线程对count进行++操作
    for( int i = 0; x<100; x++){
      new Thread(volatileAtomicThread).start();
    }
  }
}

执行结果:不保证一定是10000

问题原理说明
volatile原子性测试

代码测试

// 定义一个int 类型的变量
private volatile int count = 0;
小结

在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

在多线程环境下,要保证数据的安全性,还需要使用锁机制。

问题解决
使用锁机制

可以给count++操作添加锁,
那么count++操作就是临界区的代码,
临界区只能有一个线程去执行,
所以count++就变成了原子操作。

public class VolatileAtomicThread implements Runnable{
  
  //定义一个int类型的变量
  private volatile int count = 0;
  private static final Object obj = new Object();

  @Override
  public void run (){

    // 对该变量进行++操作,100次
    for(int x = 0; x<100; x++){
      synchronized(obj){
        count++;
        System.out.println("count ============>>>>>>> "+count);
      }
    }

  }

}
原子类

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

AtomicInteger

原子性Integer,可以实现原子更新操作

public AtomicInteger():                   初始化一个默认值为0的Integer
public AtomicInteger(int initialValue):  初始化一个指定值的原子性Integer

int get():                                获取值
int getAndIncrement():                    以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():                    以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):                  以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):                 以原子方式设置为newValue的值,并返回旧值。

演示基本使用

案例改造

使用AtomicInteger对案例进行改造

public class VolatileAtomicThread implements Runnable{

}

volatile原子性操作保障:加锁机制

volatile原子性操作保障:使用原子类

重排序的概述和好处

概述

什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

一个好的内存模型 实际上会 放松对处理器和编译器规则的束缚,
也就是说 软件技术和硬件技术都为同一个目标而进行奋斗:
在不改变程序执行结果的前提下,尽可能提高执行效率。

JMM对底层尽量减少约束,使其能够发挥自身优势。
因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令重排序,一般重排序可以分为如下三种:

  1. 编译器优化的重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

  2. 指令集并行的重排序。现代处理器采用了指令集并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序,由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

----------     -----------------------      -----------------------     ---------------------     ----------------------
| 源代码 | ---→ | 1:编译器优化重排序 |  ---→ | 2:指令集并行重排序 | ---→ | 3:内存系统重排序 | ---→ | 最终执行的指令序列 |
----------     -----------------------      -----------------------     ---------------------     ----------------------

重排序的好处

重排序可以提高处理的速度

重排序带来的问题演示

重排序案例演示

引入:重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例:

public class OutOfOrderExecution{

  private static int i = 0, j = 0;
  private static int a = 0, b = 0;

  public static void main(String[] args) throws InterruptedException{
    int count = 0;// 计数
    while(true){
      count++;
    }
  }
}

volatile禁止指令重排序

volatile修饰变量后可以实现禁止指令重排序!
volatile禁止重排序案例演示:

public class OutOfOrderException{
  //使用volatile修饰变量
  private volatile static int  i = 0, j = 0;
  private volatile static int  a = 0, b = 0;

  public static void main(String[] args)throws InterruptedException{
    int count = 0;// 计数

    while(true){
      count++;
      i = 0;
      j = 0;
      a = 0;
      b = 0;
      Thread one  = new Thread(new Runnable(){
        @Override
        public void run(){
          a = 1;
          i = b;
        }
      });
      Thread two =  new Thread(new Runnable(){
        @Override
        public void run(){

        }
      });
    }
  }
}

小结

  • 使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题。

happens-before的概述和常见规则

第三章 volatile内存语义

volatile写读建立的happens-before关系

概述

上面的内容讲述了重排序原则,为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:
如指令重排序导致的多个线程操作之间的不可见性。
如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。

从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

所以为了解决多线程的可见性问题,就提出了happens-before原则,让线程之间遵守这些原则,优化我们的语句,所以等于是给编译器优化的约束,不能让它优化的不知道东南西北了!

简单来说: happens-before应该翻译成:前一个操作的结果可以被后续的操作获取,讲白点就是:前面一个操作变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

happens-before规则

具体的一共有六项规则:

  1. 程序顺序规则(单线程规则)
  • 解释:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  • 同一个线程中前面的所有写操作对后面的操作可见
  1. 锁规则(Synchronized,Lock等)
  • 解释:对一个锁的解锁,happens-before于随后对这个锁的加锁;
  • 如果线程1解锁了monitor a,接着线程2锁定了a,那么线程1解锁a之前的写操作都对线程线程2可见(线程1和线程2可以是同一个线程)
  1. volatile变量规则:
  • 解释:对一个volatile域的写,happens-before于任意后序对这个volatile域的读
  • 如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及对线程2可见(线程1和线程2可以是同一个线程)
  1. 传递性:
  • 解释: 如果A happens-before B,且B happens-before C,那么A happens-before C。
  • A happens-before B, B happens-before C ,那么可以得到 A happens-before C
  1. start()规则:
  • 解释:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • 假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A对变量修改线程B未必可见。

volatile写读建立的happens-before规则演示

long和double的原子性问题解决方案

第四章 volatile高频面试与总结

long和double的原子性

概述

在java中,long和double都是8个字节共64位(一个字节=8bit)
那么如果是一个32位的系统,读写long或double的变量时会涉及到原子性问题,
因为32位的系统要读完64位的变量,需要分两步执行,每次读取32位,这样对double和long变量的赋值就会出现问题:

如果两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。

案例演示
public class LongTest implements Runnable{
  private static long aLong = 0;
  private volatile long value;

  public LongTest(long value){
    this.setValue(value);
  }

  @Override
  public void run(){
    int i = 0;
    while( i < 100000){
      LongTest along = this.getValue();
      i++;
      long temp = LongTest.aLong;
      if(temp != 1L && temp != -1L){
        System.out.println("出现错误结果"+ temp);
        System.exit(0);
      }
    }
    System.out.println("运行正确");
  }

  public static void main(String[] args)throws InterruptedException{

  }
}

单例、懒汉、饿汉单例的概念

volatile在双重检查加锁的单例中的应用

单例模式有八种

单例模式可以提供出8中写法,有很多时候存在饿汉式单例的概念,以及懒汉式单例的概念。

饿汉式单例的含义是:在获取单例对象之前对象已经创建完成了。
懒汉式单例是指:在真正需要单例的时候才创建出该对象。

饿汉式单例的2种写法

饿汉单例的两种写法

饿汉式(静态常量)
/**
  * 饿汉式(静态常量)
  */
  public class Singleton1{
    private final static Singleton1 INSTANCE = new Singleton1();
    private Singleton1{

    }
    public static Singleton1 getInstance(){
      return INSTANCE;
    }
  }
饿汉式(静态代码块)

懒汉式单例的2种写法与线程安全分析

懒汉式单例四种写法

懒汉式(线程不安全)
懒汉式(线程安全)
懒汉式(线程不安全)
懒汉式(volatile双重检查模式 推荐)
案例代码
/**
 * 描述: 双重检查,推荐面试中进行使用。
 */
 public class Singleton6{
   // 静态属性,volatile保证可见性和禁止指令重排序
   private volatile static Singleton6 instance = null;

   // 私有化构造器
   private Singleton6(){}

   public static Singleton6 getInstance(){

     //第一重检查锁定
     if(instance == null){

       // 同步锁定代码块
       synchronized(Singleton6.class){

         //第二重检查锁定
          if(instance == null){

            //注意:非原子操作
            instance = new Singleton6();
          }
       }
     }
     return instance;
   }
 }
分析

双重检查的优点:线程安全,延迟加载,效率较高!

为何要使用volatile保证安全?

1、禁止指令重排序

  • 对象实际上创建对象要经过如下几个步骤:
a. 看class对象是否加载,如果没有就先加载class对象
b. 分配内存空间,初始化示例。
c. 调用构造函数
d. 返回地址给引用
静态内部类单例方式
枚举实现单例

懒汉式单例性能优化后的进阶写法

懒汉式双重检查模式与volatile修饰

懒汉式静态内部类单例方式

枚举实现单例思想介绍

volatile的使用场景之一:纯赋值操作

volatile的使用场景

纯赋值操作

概述

volatile不适合做a++。
适合做纯赋值操作:如boolean flag = false/true;

案例代码
public class UseVolatile1 implements Runnable{

  volatile boolean flag = false;
  AtomicInteger realA = new AtomicInteger();

  public static void main(String[] args)throws InterruptedException{
    UseVolatile1 r  = new UseVolatile1();
    Thread thread1 = new Thread(r);
    Thread thread2 = new Thread(r);
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    System.out.println(r.flag);
    System.out.println(r.realA.get());
  }

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

  private void setDone(){
    flag = true; // 纯赋值操作符合预期
    // flag = !flag; // 这样做不符合预期
  }
}
小结

volatile 可以适合做多线程中的纯赋值操作;
如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,
那么就可以用volatile来代替synchronized或者代替原子变量。
因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

volatile的第二种使用场景:触发器

概念

按照volatile的可见性和禁止重排序以及happens-before规则。
volatile可以作为刷新之前变量的触发器。
可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,
触发获取到该变量之前的操作都将是最新的且可见。

案例演示

public class VisibilityHP{

    int a = 1;
    int b = 2;
    int c = 3;
    volatile boolean flag = false;

    private void write(){
      a = 3;
      b = 4;
      c = a;
      flag = true;
    }

    private void read(){
      // flag被volatile修饰,充当了触发器,一旦值为true,此处立即对变量之前的操作可见。
      while(flag){
        System.out.println("a = "+ a + "; b = "+ b + "; c = "+ c);
      }
    }
    public static void main(String[] args) {
      while(true){
        VisibilityHP test = new VisibilityHP();
        new Thread(new Runnable(){
          try{
            Thread.sleep(100);
          }catch(InterruptedException e){

          }
        });
      }
    }
}

volatile与synchronized的区别说明

区别

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法以及代码块。
  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
  • volatile用于禁止指令重排序:可以解决单例双重检查乱序问题。
  • volatile可看作是轻量版的synchronized,volatile不保证原子性;但是如果对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了一致性,所以就可以保证线程安全了;

volatile总体总结说明

总体总结

  1. volatile 修饰符 用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  3. volatile只能作用域属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主内存中读取。
  5. volatile提供了happens-before保证对volatile变量的写入,happens-before所有其他线程后续对V的操作。
  6. volatile可以使得long和double的赋值时原子的。
  7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值