Volatile特性之 不保证原子性

怎么导致原子性?
通过前面对JMM的介绍,我们知道,
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值,但是还未写入主内存时,
另外一个线程BBB又对主内存中同一共享变量X进行操作,
但此时A线程工作内存中共享变量X对线程B来说是不可见,
这种工作内存与主内存同步延迟现象就造成了可见性问题。
原子性
不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。
数据库也经常提到事务具备原子性
代码测试
为了测试volatile是否保证原子性,我们创建了20个线程,然后每个线程分别循环1000次,来调用number++的方法
MyData myData = new MyData();

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

最后通过 Thread.activeCount(),来感知20个线程是否执行完毕,这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程

// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
while(Thread.activeCount() > 2) {
    // yield表示不执行
    Thread.yield();
}

然后在线程执行完毕后,我们在查看number的值,假设volatile保证原子性的话,那么最后输出的值应该是,20 * 1000 = 20000,
完整代码如下所示:


```java
/**
 * Volatile Java虚拟机提供的轻量级同步机制
 *
 * 可见性(及时通知)
 * 不保证原子性
 * 禁止指令重排
 *
 */
import java.util.concurrent.TimeUnit;

/**
 * 假设是主物理内存
 */
class MyData {
    /**
     * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
     */
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    /**
     * 注意,此时number 前面是加了volatile修饰
     */
    public void addPlusPlus() {
        number ++;
    }
}

/**
 * 验证volatile的可见性
 * 1、 假设int number = 0, number变量之前没有添加volatile关键字修饰
 * 2、添加了volatile,可以解决可见性问题
 *
 * 验证volatile不保证原子性
 * 1、原子性指的是什么意思?
 */
public class VolatileDemo {

    public static void main(String args []) {

        MyData myData = new MyData();

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

        // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
        // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
        while(Thread.activeCount() > 2) {
            // yield表示不执行
            Thread.yield();
        }

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

    }
}

最终结果我们会发现,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
}

这里查看字节码的操作,是用到了IDEA的javap命令

我们首先,使用IDEA提供的External Tools,来扩展javap命令

完成上述操作后,我们在需要查看字节码的文件下,右键选择 External Tools即可

如果出现了找不到指定类,那是因为我们创建的是spring boot的maven项目,我们之前需要执行mvn package命令,进行打包操作,将其编译成class文件

移动到底部,有一份字节码指令对照表,方便我们进行阅读

下面我们就针对 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 addPlusPlus() {
        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.addPlusPlus();
                myData.addAtomic();
            }
        }, String.valueOf(i)).start();
    }

最后输出

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

下面的结果,一个是引入synchronized,一个是使用了原子包装AtomicInteger

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值