一、原子性
原子性操作指相应的操作是单一不可分割的操作。在我们学化学这门课程的时候,对于里面讲到的原子性相信大家都非常明白,原子是微观世界中最小的不可再进行分割的单元,原子是最小的粒子。java里面的原子性操作也是如此,它代表着一个操作不能再进行分割是最小的执行单元,或者一系列操作要么全部成功执行,要么全部执行失败,不允许中间某一些成功失败,类比如事物控制,要么全部提交要么全部回滚。 下面根据几个粒子来分析下原子性操作:
i = 0; //1 j = i ; //2 i++; //3 i = j + 1; //4
上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。
1在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 2中包含了两个操作:读取i,将i值赋值给j 3中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 4中同三一样
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。在多线程环境中,非原子操作可能会受其他线程的干扰,例如第3个操作,i在加1之后将结果赋值给i,在赋值给i回写主内存的时候可能会被其他线程抢先回写,导致此次执行失败丢失了本次计算结果(这里会涉及到原子性操作,下面会进行讲解)。
public class AtomicTest { private int i = 0; public void add() { i++; } public static void main(String[] args) { for (int t = 0; t < 10; t++) { AtomicTest test = new AtomicTest(); Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int k = 0; k < 1000; k++) { test.add(); } }); threads[i].start(); } Arrays.stream(threads).forEach(th -> { try { th.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("第" + (t + 1) + "次执行结果:" + test.i); } } }
第1次执行结果:8987第2次执行结果:8970第3次执行结果:6820第4次执行结果:9841第5次执行结果:10000第6次执行结果:7766第7次执行结果:8105第8次执行结果:10000第9次执行结果:10000第10次执行结果:10000
最终的执行结果会是小于等于10000,在某些情况下与我们所期望的结果10000不符合,并发的情况下导致bug的产生。
要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。volatile是无法保证复合操作的原子性。
二、可见性
int i = 0; //语句1 boolean flag = false; //语句2 i = 1; //语句3 flag = true; //语句4
上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行。此种指令重排之后单线程下不会有问题,单如果是在多线程的情况下呢?
public class SerialTest { static SerialTest serialTest; static boolean isInit = false; public static void main(String[] args) { for(int i=0; i< 200;i++) { serialTest = null; isInit = false; new Thread(()->{ serialTest = new SerialTest();//语句1 isInit = true; //语句2 }).start(); new Thread(()->{ if(isInit) { serialTest.doSomething(); } }).start(); } } public void doSomething() { System.out.println("doSomething"); } }
Exception in thread "Thread-283" java.lang.NullPointerException at com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25) at java.lang.Thread.run(Thread.java:748) ...... doSomething doSomething doSomething doSomething doSomething Exception in thread "Thread-283" java.lang.NullPointerException at com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25) at java.lang.Thread.run(Thread.java:748)
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。