volatile之不保证原子性

830 阅读2分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

volatile无法保证原子性,那么什么是原子性呢?

原子性

事务是最小的执行单位,不允许分割。事务的原⼦性确保动作要么全部成功,要么全部失败,这就叫原子性。

什么是原子操作?

就是无法被别的线程打断的操作。要么不执行,要么就执行成功。

volatile不保证原子性的代码证明

public class Code02_VolatileNotAtomic {


    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myTask.sell();
                }
            }, String.valueOf(i)).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();  //当线程使用了这个方法之后,它就会把自己CPU执行的时间让掉
        }
        System.out.println("finally tickets : " + myTask.tickets);
    }
}

class MyTask {
    volatile int tickets = 0;

    public void sell() {
        tickets++;
    }
}

运行结果好像上应该是10000,可是我们看下实际的运行结果

image.png

这是由于tickets++这个操作实际是是分为3步的:

  1. 将tickets变量从主内存拷贝到工作内存中
  2. 在工作内存中将副本的tickets+1
  3. 将修改完的值写回的主内存当中

那么就是在这个过程中出现了写覆盖的情况,比如线程A和线程B都将主内存中的值拷贝到自己的工作内存中进行+1的操作,假设原来tickets=0,那么A在修改完成之后要把tickets=1写回到主内存中,但是由于某种原因A线程被挂起,此次线程B将tickets=1写入到主内存当中,写回之后还没来得及通知线程A,被挂起的A线程继续执行将tickets=1又写入到主内存中,本来经过两个线程的操作tickets=2可是现在tickets=1,这就是为什么实际的运行结果小于10000

下面写一个测试类

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

    }
    int num = 0;

    public void addNum() {
        num++;
    }
}

反编译该类的字节码文件,查看addNum方法部分

public void addNum();
    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

那么怎么解决这个问题呢

1.在sell方法上加synchronized(不推荐,因为synchronized是重量级锁)

2.使用java.util.concurrent.atomic包下的AtomicInteger类

class MyTask {
    AtomicInteger tickets = new AtomicInteger();

    public synchronized void sell() {
        tickets.getAndIncrement();
    }
}