「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,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,可是我们看下实际的运行结果
这是由于tickets++这个操作实际是是分为3步的:
- 将tickets变量从主内存拷贝到工作内存中
- 在工作内存中将副本的tickets+1
- 将修改完的值写回的主内存当中
那么就是在这个过程中出现了写覆盖的情况,比如线程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();
}
}