一, 加锁与无锁 CAS
在谈论无锁概念时, 总会关联起乐观派与悲观派, 对于乐观派而言, 他们认为事情总会往好的方向发展, 总是认为坏的情况发生的概率特别小, 可以无所顾忌地做事, 但对于悲观派而已, 他们总会认为发展事态如果不及时控制, 以后就无法挽回了, 即使无法挽回的局面几乎不可能发生. 这两种派系映射到并发编程中就如同无锁与加锁的策略, 即加锁是一种悲观策略, 无锁是一种乐观策略, 因为对于加锁的并发程序来说, 它们总是认为每次访问共享资源时总会发生冲突, 因此必须对每一次数据操作实施加锁策略. 而无锁则总是假设对共享资源的访问没有冲突, 线程可以不停执行, 无需加锁, 无需等待, 一旦发现冲突, 无锁策略则采用一种称为 CAS 的技术来保证线程执行的安全性, 这项 CAS 技术就是无锁策略实现的关键, 下面我们进一步了解 CAS 技术的奇妙之处.
二, CAS
CAS 的全称是 Compare And Swap 即比较交换, 其算法核心思想如下
执行函数: CAS(V,E,N) V 表示要更新的变量, E 表示预期值, N 表示新值
如果 V 值等于 E 值, 则将 V 的值设为 N. 若 V 值和 E 值不同, 则说明已经有其他线程做了更新, 则当前线程什么都不做. 通俗的理解就是 CAS 操作需要我们提供一个期望值, 当期望值与当前线程的变量值相同时, 说明还没线程修改该值, 当前线程可以进行修改, 也就是执行 CAS 操作, 但如果期望值与当前线程不符, 则说明该值已被其他线程修改, 此时不执行更新操作, 但可以选择重新读取该变量再尝试再次修改该变量, 也可以放弃操作, 原理图如下
由于 CAS 操作属于乐观派, 它总认为自己可以成功完成操作, 当多个线程同时使用 CAS 操作一个变量时, 只有一个会胜出, 并成功更新, 其余均会失败, 但失败的线程并不会被挂起, 仅是被告知失败, 并且允许再次尝试, 当然也允许失败的线程放弃操作, 这点从图中也可以看出来. 基于这样的原理, CAS 操作即使没有锁, 同样知道其他线程对共享资源操作影响, 并执行相应的处理措施. 同时从这点也可以看出, 由于无锁操作中没有锁的存在, 因此不可能出现死锁的情况, 也就是说无锁操作天生免疫死锁.
或许我们可能会有这样的疑问, 假设存在多个线程执行 CAS 操作并且 CAS 的步骤很多, 有没有可能在判断 V 和 E 相同后, 正要赋值时, 切换了线程, 更改了值. 造成了数据不一致呢? 答案是否定的, 因为 CAS 是一种系统原语, 原语属于操作系统用语范畴, 是由若干条指令组成的, 用于完成某个功能的一个过程, 并且原语的执行必须是连续的, 在执行过程中不允许被中断, 也就是说 CAS 是一条 CPU 的原子指令, 不会造成所谓的数据不一致问题.
Unsafe 类存在于 sun.misc 包中, 其内部方法操作可以像 C 的指针一样直接操作内存, 单从名称看来就可以知道该类是非安全的, 毕竟 Unsafe 拥有着类似于 C 的指针操作, 因此总是不应该首先使用 Unsafe 类, Java 官方也不建议直接使用的 Unsafe 类, 但我们还是很有必要了解该类, 因为 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法, 注意 Unsafe 类中的所有方法都是 native 修饰的, 也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应任务, 关于 Unsafe 类的主要功能点如下:
三, CAS 在 Java 中的应用
并发包中的原子操作类 (Atomic 系列) 底层就是 CAS. 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包, 在该包中提供了许多基于 CAS 实现的原子操作类, 用法方便, 性能高效.
AtomicBoolean: 原子更新布尔类型, AtomicInteger: 原子更新整型, AtomicLong: 原子更新长整型
这 3 个类的实现原理和使用方式几乎是一样的, 这里我们以 AtomicInteger 为例进行分析, AtomicInteger 主要是针对 int 类型的数据执行原子操作, 它提供了原子自增方法, 原子自减方法以及原子赋值方法等, 如果查看源码, 可以发现 AtomicInteger 原子类的内部几乎是基于前面分析过 Unsafe 类中的 CAS 相关操作的方法实现的, 这也同时证明 AtomicInteger 是基于无锁实现的, 这里重点分析自增操作实现过程, 其他方法自增实现原理一样.
AtomicStampedReference 类, AtomicStampedReference 原子类是一个带有时间戳的对象引用, 在每次修改后, AtomicStampedReference 不仅会设置新值而且还会记录更改的时间. 当 AtomicStampedReference 设置对象值时, 对象值以及时间戳都必须满足期望值才能写入成功, 这也就解决了反复读写时, 无法预知值是否已被修改的窘境.
AtomicMarkableReference 类, AtomicMarkableReference 与 AtomicStampedReference 不同的是, AtomicMarkableReference 维护的是一个 boolean 值的标识, 也就是说至于 true 和 false 两种切换状态, AtomicMarkableReference 的实现原理与 AtomicStampedReference 类似, 这里不再介绍. 到此, 我们也明白了如果要完全杜绝 ABA 问题的发生, 我们应该使用 AtomicStampedReference 原子类更新对象, 而对于 AtomicMarkableReference 来说只能减少 ABA 问题的发生概率, 并不能杜绝.
四, 自旋锁
自旋锁是一种假设在不久将来, 当前的线程可以获得锁, 因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因), 在经过若干次循环后, 如果得到锁, 就顺利进入临界区. 如果还不能获得锁, 那就会将线程在操作系统层面挂起, 这种方式确实也是可以提升效率的. 但问题是当线程越来越多竞争很激烈时, 占用 CPU 的时间变长会导致性能急剧下降, 因此 Java 虚拟机内部一般对于自旋锁有一定的次数限制, 可能是 50 或者 100 次循环后就放弃, 直接挂起线程, 让出 CPU 资源.
使用 CAS 原子操作作为底层实现, lock()方法将要更新的值设置为当前线程, 并将预期值设置为 null.unlock()函数将要更新的值设置为 null, 并预期值设置为当前线程. 然后我们通过 lock()和 unlock 来控制自旋锁的开启与关闭, 注意这是一种非公平锁. 事实上 AtomicInteger(或者 AtomicLong)原子类内部的 CAS 操作也是通过不断的自循环 (while 循环) 实现, 不过这种循环的结束条件是线程成功更新对于的值, 但也是自旋锁的一种.
使用 CAS 原子操作作为底层实现, lock()方法将要更新的值设置为当前线程, 并将预期值设置为 null.unlock()函数将要更新的值设置为 null, 并预期值设置为当前线程. 然后我们通过 lock()和 unlock 来控制自旋锁的开启与关闭, 注意这是一种非公平锁. 事实上 AtomicInteger(或者 AtomicLong)原子类内部的 CAS 操作也是通过不断的自循环 (while 循环) 实现, 不过这种循环的结束条件是线程成功更新对于的值, 但也是自旋锁的一种.
来源: http://www.bubuko.com/infodetail-3028661.html