一、原子性
原子,一个不可再被分割的颗粒。原子性,指的是一个或多个不能再被分割的操作。
int i = 1; // 原子操作
i++; // 非原子操作,从主内存读取 i 到线程工作内存,进行 +1,再把 i 写到朱内存。
虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为“复合操作”。
我们可以用synchronized
或 Lock
来把这个复合操作“变成”原子操作。
栗子:
1 2 3 | private synchronized void increase(){ i++; } |
或
1 2 3 4 5 6 7 8 9 10 11 | private int i = 0; Lock mLock = new ReentrantLock(); private void increase() { mLock.lock(); try { i++; } finally{ mLock.unlock(); } } |
这样我们就可以把这个一个方法看做一个整体,一个不可分割的整体。
除此之前,我们还可以用java.util.concurrent.atomic
里的原子变量类,可以确保所有对计数器状态访问的操作都是原子的。
栗子:
1 2 3 4 5 | AtomicInteger mAtomicInteger = new AtomicInteger(0); private void increase(){ mAtomicInteger.incrementAndGet(); } |
二、可见性
当多线程访问某一个(同一个)变量时,其中一条线程对此变量作出修改,其他线程可以立刻读取到最新修改后的变量。
1 2 3 4 5 6 | int i = 0; // 线程 1 执行 i++; // 线程 2 执行 System.out.print("i=" + i); |
即使是在执行完线程里的 i++ 后再执行线程 2,线程 2 的输入结果也会有 2 个种情况,一个是 0 和 1。
因为 i++ 在线程 1(CPU1)中做完了运算,并没有立刻更新到主内存当中,而线程 2(CPU2)就去主内存当中读取并打印,此时打印的就是 0。
synchronized
和Lock
能够保证可见性。
另外volatile
关键字也可以解决这个问题(下一篇会讲到)。
三、有序性
我们都知道处理器为了拥有更好的运算效率,会自动优化、排序执行我们写的代码,但会确保执行结果不变。
栗子:
1 2 3 4 | int a = 0; // 语句 1 int b = 0; // 语句 2 i++; // 语句 3 b++; // 语句 4 |
这一段代码的执行顺序很有可能不是按上面的 1、2、3、4 来依次执行,因为 1 和 2 没有数据依赖,3 和 4 没有数据依赖, 2、1、4、3 这样来执行可以吗?完全没问题,处理器会自动帮我们排序。
在单线程看来并没有什么问题,但在多线程则很容易出现问题。
再来个栗子:
1 2 3 4 5 6 7 8 | // 线程 1 init(); inited = true; // 线程 2 while(inited){ work(); } |
init(); 与 inited = true; 并没有数据的依赖,在单线程看来,如果把两句的代码调换好像也不会出现问题。
但此时处于一个多线程的环境,而处理器真的把这两句代码重新排序,那问题就出现了,若线程 1 先执行 inited = true; 此时,init() 并没有执行,线程 2 就已经开始调用 work() 方法,此时很可能造成一些奔溃或其他 BUG 的出现。
synchronized
和Lock
能确保原子性,能让多线程执行代码的时候依次按顺序执行,自然就具有有序性。
而volatile
关键字也可以解决这个问题,volatile 关键字可以保证有序性,让处理器不会把这行代码进行优化排序。
题外话:
本人将打算写一系列的多线程并发实战文章,用于自己对多线程的新知识学习和对旧知识的回顾作出记录、总结。
过程当中会不断对之前写的文章作出补充。
若看到有理解不当之处敬请指正。
这一系列文章将会用到本人于 2017 年 3 月 12 日拍摄的一系列“弓道,剑道”作品作为封面图。
出境:小夏
坐标:华农树木园
并发,就像万剑齐发。
并发,就像剑一样的利器,好好运用,可以解决许多头疼问题。