欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Volatile不保证原子性

程序员文章站 2022-03-15 19:38:50
...

Volatile不保证原子性

前言

可见性解析: 链接.

通过前面对JMM的介绍,我们知道,各个线程对主内存*享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值,但是还未写入主内存时,另外一个线程BBB又对主内存中同一共享变量X进行操作,但此时A线程工作内存*享变量X对线程B来说是不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

原子性

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

数据库也经常提到事务具备原子性

代码测试

为了测试volatile是否保证原子性,我们创建了20个线程,然后每个线程分别循环1000次,来调用number++的方法

package com.qcby.bilbil.gc;

import java.util.concurrent.TimeUnit;

/**
 * @author HuangHaiyang
 * @date 2020/08/18
 * @description: description
 * @version: 1.0.0
 */


class MyData{
    volatile int number=0;

    public void addNumber(){
        number++;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData=new MyData();

        // 创建20个线程,线程里面进行1000次循环
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    myData.addNumber();
                }
            },i+"线程").start();
        }

        //java默认的线程 main线程和 GC线程,活动线程数>2 说明上面代码还没执行完
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("最终的number值为:"+myData.number);


    }
}

假设volatile保证原子性的话,那么最后输出的值应该是 20 * 1000 = 20000,

最终结果我们会发现,number输出的值并没有20000,而且是每次运行的结果都不一致的,这说明了volatile修饰的变量不保证原子性

为什么出现数值丢失

各自线程在写入主内存的时候,出现了数据的丢失,而引起的数值缺失的问题

下面我们将一个简单的number++操作,转换为字节码文件一探究竟

public class T1 {
    volatile int n = 0;
    public void add() {
        n++;
    }
}

转换后的字节码文件

public class com.moxi.interview.study.thread.T1 {
  volatile int n;

  public com.moxi.interview.study.thread.T1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field n:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field n:I
      10: return
}

下面我们就针对 add() 这个方法的字节码文件进行分析

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2    // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2    // Field n:I
      10: return

我们能够发现 n++这条命令,被拆分成了3个指令

  • 执行getfield 从主内存拿到原始n
  • 执行iadd 进行加1操作
  • 执行putfileld 把累加后的值写回主内存

假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000

如何解决

因此这也说明,在多线程环境下 number ++ 在多线程环境下是非线程安全的,解决的方法有哪些呢?

  • 在方法上加入 synchronized
    public synchronized void addNumber() {
        number ++;
    }

我们能够发现引入synchronized关键字后,保证了该方法每次只能够一个线程进行访问和操作,最终输出的结果也就为20000

其它解决方法

上面的方法引入synchronized,虽然能够保证原子性,但是为了解决number++,而引入重量级的同步机制,有种 杀鸡焉用牛刀

除了引用synchronized关键字外,还可以使用JUC下面的原子包装类,即刚刚的int类型的number,可以使用AtomicInteger来代替

    /**
     *  创建一个原子Integer包装类,默认为0
      */
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        // 相当于 atomicInter ++
        atomicInteger.getAndIncrement();
    }

然后同理,继续刚刚的操作

        // 创建10个线程,线程里面进行1000次循环
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }

最后输出

 // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
 System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + myData.atomicInteger);

最终解决方法
一是引入synchronized,
二是使用了原子包装类AtomicInteger

相关标签: 面试专栏

上一篇: 简单的文件搜索

下一篇: 迭代器