信息发布→ 登录 注册 退出

Java中多线程与并发_volatile关键字的深入理解

发布时间:2026-01-10

点击量:
目录
  • 一、volatile关键字
  • 二、JMM(Java Memory Model)
  • 三、验证
    • 1.验证volatile的可见性
  • 2.验证volatile不保证原子性
    • 2.1 原子性指的是什么意思?
    • 2.2 volatile不保证原子性的案例演示
    • 2.3 为什么不保证原子性?
    • 2.4 如何保证原子性
  • 总结

    一、volatile关键字

    volatile是JVM提供的一种轻量级的同步机制,特性:

    1.保证内存可见性

    2.不保证原子性

    3.防止指令重排序

    二、JMM(Java Memory Model)

    Java内存模型中规定了所有的变量都存储在主内存中(如虚拟机物理内存中的一部分),每条线程还有自己的工作内存(如CPU中的高速缓存),线程的工作内存中保存了该线程使用到的变量到主内存的副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存和工作内存的交互关系如下图所示:

    三、验证

    1.验证volatile的可见性

    1.1 假如 int num = 0; num变量之前根本没有添加volatile关键字修饰,没有可见性

    1.2 添加了volatile,可以解决可见性问题

    MyData类

    class MyData {
     volatile int num = 0;
    
     public void addT060() {
     this.num = 60;
     }
    }
    

    内存可见性验证,其中两个线程分别为AAA线程和main线程

     //volatile可以保证可见性,及时通知其它线程,主内存的值已经被修改
     @Test
     public void seeOkByVolatile() {
     MyData myData = new MyData();//资源类
    
     new Thread(() -> {
      System.out.println(Thread.currentThread().getName() + "\t come in");
      //暂停一会线程
      try{
      TimeUnit.SECONDS.sleep(3);
      }catch (InterruptedException e) {
      e.printStackTrace();
      }
      myData.addT060();
      System.out.println(Thread.currentThread().getName() + "\t update num value: " + myData.num);
     },"AAA").start();
    
     //第2个线程是我们的main线程
     while (myData.num == 0) {
      //main线程就一直在这里等待循环,直到num值不再等于0.
     }
     System.out.println(Thread.currentThread().getName() + "\t mission is over,main get num value: " + myData.num );
     }
    

    对num变量加volatile修饰后结果

    AAA come in
    AAA update num value: 60
    main 我能见到AAA线程对num修改的结果啦,main get num value: 60

    Process finished with exit code 0

    2.验证volatile不保证原子性

    2.1 原子性指的是什么意思?

    不可分割,完整性,也即某个线程正在做某个具体任务时,中间不可以被加塞或者被分割。需要整体完整。要么同时成功,要么同时失败。

    2.2 volatile不保证原子性的案例演示

    2.3 为什么不保证原子性?

    2.4 如何保证原子性

    加sync

    使用我们juc下的AtomicInteger (底层实现CAS)

    给MyData类加addPlusPlus()方法

    class MyData {//MyData.java ===> MyData.class ===> JVM字节码
     int num = 0;
    
     public void addT060() {
     this.num = 60;
     }
    
     //请注意,此时num前面是加了关键字修饰的,volatile不保证原子性
     public void addPlusPlus() {
     num++;
     }
    }
    

    2.2 volatile不保证原子性的案例演示

    num++在多线程操作的情况下不保证原子性的

    创建20个线程并行执行num++操作2000次,多次测试,结果不为40000

    public static void main(String[] args) {
     MyData myData = new MyData();
    
     for (int i = 1; i <= 20; i++ ) {
    
      new Thread(() -> {
      for (int j = 1; j <= 2000; j++) {
       myData.addPlusPlus();
      }
    
      },String.valueOf(i)).start();
     }
    
     //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
     while(Thread.activeCount() > 2) {
      Thread.yield();
     }
    
     System.out.println(Thread.currentThread().getName() + "\t finally num value:" + myData.num);
     }
    

    结果:数值小于40000,出现写值丢失的情况

    main  finally num value:38480

    Process finished with exit code 0

    2.3 为什么不保证原子性?

    因为当线程A对num++操作从自己的工作内存刷新到主内存时,还未通知到其他线程主内存变量有更新的瞬间,其他线程对num变量的操作结果也对主内存进行了刷新,从而导致了写值丢失的情况

    num++通过汇编指令分析,通过javap反编译得到如下汇编指令

    class com.slx.juc.MyData {
     volatile int num;
    
     com.slx.juc.MyData();
     Code:
     0: aload_0
     1: invokespecial #1   // Method java/lang/Object."<init>":()V
     4: aload_0
     5: iconst_0
     6: putfield #2   // Field num:I
     9: return
    
     public void addT060();
     Code:
     0: aload_0
     1: bipush 60
     3: putfield #2   // Field num:I
     6: return
    
     public void addPlusPlus();
     Code:
     0: aload_0
     1: dup
     2: getfield #2   // Field num:I
     5: iconst_1
     6: iadd
     7: putfield #2   // Field num:I
     10: return
    }
    

    可见num++被拆分成了3个步骤,简称:读-改-写

    • 执行getfield拿到原始num;
    • 执行iadd进行加1操作;
    • 执行putfield写把累加后的值写回

    2.4 如何保证原子性

    加sync

    使用我们juc下的AtomicInteger (底层实现CAS)

    MyData类中添加原子类操作方法

     AtomicInteger atomicInteger = new AtomicInteger();
     public void addMyAtomic() {
     atomicInteger.getAndIncrement();
     }
    

    调用该方法打印结果

     public static void main(String[] args) {
     MyData myData = new MyData();
    
     for (int i = 1; i <= 20; i++ ) {
    
      new Thread(() -> {
      for (int j = 1; j <= 2000; j++) {
       myData.addMyAtomic();
      }
    
      },String.valueOf(i)).start();
     }
    
     //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
     while(Thread.activeCount() > 2) {
      Thread.yield();
     }
    
     System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type ,finally num value:" + myData.atomicInteger);
     }
    

    测试结果为40000,不会出现之前int类型的丢失值的情况

    main  AtomicInteger type ,finally num value:40000

    Process finished with exit code 0

    总结

    在线客服
    服务热线

    服务热线

    4008888355

    微信咨询
    二维码
    返回顶部
    ×二维码

    截屏,微信识别二维码

    打开微信

    微信号已复制,请打开微信添加咨询详情!