同一时刻只有一个线程执行,我们称之为互斥,如果我们能够保证对共享变量的修改是互斥的,那么就能保证原子性了。
加锁
锁是一种通用的技术方案,Java提供的 synchronized 关键字,就是锁的一种实现。
synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!sychronized 并不能改变CPU时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待。
synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码后,无论是单核CPU还是多核CPU,只有一个线程能够执行该代码。
synchronized 也能够保证可见性和有序性。
原子变量
现在我们已经知道互斥锁可以保证原子性,也知道了如何使用 sychronized 来保证原子性,但 sychronized并不是Java唯一能保证原子性的方案。
如果你粗略的看一下JUC(java.util.concurrent包),那么你可以很显眼的发现它俩:
一个是locks包,一个是atomic包,它们可以解决原子性问题。加锁是一种阻塞式方式实现,而原子变量是非阻塞式方式实现。
原子类原理(以AtomicInteger为例)
原子类的原子性是通过 volatile + CAS 机制实现原子操作的。AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value 的内存可见性,这为后续的 CAS 实现提供了基础。低并发情况下使用AtomicInteger。
CAS机制
CAS
比较并交换,该算法是硬件对于并发操作的支持。CAS 是乐观锁的一种实现,他采用自旋的思想,是一种轻量级的锁机制。即每次判断预估值和内存中的值是不是相同,如果不相同则说明该内存值已经被其他线程更新过,因此需要拿到该最新值作为预估值,重新判断。而该线程不断的循环判断该内存值是否已被其他线程更新过,这就是自旋的思想。底层是通过 Unsafe 类中的compareAndSwapInt 等方法实现。
CAS 包含了三个操作数:
① 内存值 V
② 预估值 A (比较时,从内存中再次读到的值)
③ B (更新后的值)
当且仅当预期值 A == V 时, V = B,否则什么都不做。
这种做法的效率高于加锁。当一个线程要进行++操作时,先从内存中取出共享变量,记录一个预估值,将工作内存中++后的值写入主内存前,要判断预估值和主内存中的值是否一致,如果一致,就写入++后的值,如果不一致,说明其他线程修改过,需要重新获取主内存的共享变量,重复之前的操作。
CAS 的缺点
原子类内部实现使用了不加锁的CAS机制,线程不会被阻塞,不断的自旋会导致CPU的消耗,在并发量大的时候容易导致CPU跑满。