volatile不保证原子性

volatile不保证原子性代码演示

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

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

原子性

不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。
数据库也经常提到事务具备原子性

代码测试

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

class MyDate1{
    volatile int  number = 0;
    public void addTO60(){
        this.number = 60;
    }
    //此时number前面加了volatile关键字修饰的,volatile不保证原子性
    public void addPlusPlus(){
        number++;
    }
}

/**
 * 不保证原子性的案例演示
 */
public class VolatileDemo2 {
    public static void main(String[] args) {
        MyDate1 myDate1 = new MyDate1();
        for (int i = 1; i <=20 ; i++) {
            new Thread(()->{
                for (int j = 1; j <=1000; j++) {
                    myDate1.addPlusPlus();
                }
            }).start();
        }
        //需要等待上面20个线程全部计算完成后,再加main线程取得最终的结果值看是多少?
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value"+ myDate1.number);
    }
}

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

main	 finally number value19438

不能保证原子性

加入synchronized后会保证原子性

class MyDate1{
    volatile int  number = 0;
    public void addTO60(){
        this.number = 60;
    }
    //此时number前面加了volatile关键字修饰的,volatile不保证原子性
    public synchronized void addPlusPlus(){
        number++;
    }
}
main	 finally number value20000

为什么会出现数据丢失

不保证原子性理论解释


对add()这个方法的字节码文件进行分析:
被拆分了3个指令

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

如何解决原子性

synchronized

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

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

运行结果:

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

其他解决方法

上面的方法引入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

posted @ 2020-08-16 16:51  柒丶月  阅读(314)  评论(0)    收藏  举报