为解决线程安全问题,互斥同步相当于以时间换空间。多线程情况下,只有一个线程可以访问同步代码。这种同步也叫阻塞同步(Blocking Synchronization).
这种同步属于一种悲观并发策略。认为只要不同步,共享数据就会被并发访问。随着硬件指令集的发展,我们可以采用基于冲突检测的乐观并发策略。
先进行操作,如果没有其他线程操作共享数据,就操作成功;否则采取补偿措施,去重试直到成功。这种策略不需要把线程挂起,因此称为非阻塞同步。(Non-Blocking Synchrinization)
要保证两个操作的原子性,需要借助处理器指令来完成。这类指令有:
测试并设置(test-and-set)
获取并增加(fetch-and-increment)
交换(swap)
比较并交换(compare-and-swap,简称 CAS)
加载链接、条件存储(load-linked/store-conditional)
CAS 指令需要有 3 个操作数,分别为内存位置(V,变量的内存地址),旧的预期值(A),和新值(B)。CAS 指令执行时,当且仅当 V 的值复合旧的预期值时,处理器使用 B 更新 V 值。否则不更新,上述操作是一个原子操作。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
Java 中的 CAS 操作:
public class CasTest { //private Integer count = 0; private final AtomicInteger count = new AtomicInteger(0); private ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 20, 10, TimeUnit.MINUTES, new ArrayBlockingQueue(100)); private void increase() { // count++; count.incrementAndGet(); } @Test public void testMutiThreadAdd() { for (int i = 0; i < 5; i++) { executor.execute(() -> { for (int j = 0; j < 1000; j++) { increase(); } }); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(count); }}
使用 atomicInteger 后,每次都能输出一致的结果。increamentAndGet( ) 通过 CAS 保证了 自增操作的原子性;
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作.
参考: 深入理解Java 虚拟机