青岛网景互联网站建设公司,中山网站建设文化策划书,毕业设计 网站建设选题,网站的关键词怎么选择引言
传统的并发控制手段#xff0c;如使用synchronized关键字或者ReentrantLock等互斥锁机制#xff0c;虽然能够有效防止资源的竞争冲突#xff0c;但也可能带来额外的性能开销#xff0c;如上下文切换、锁竞争导致的线程阻塞等。而此时就出现了一种乐观锁的策略#x…引言
传统的并发控制手段如使用synchronized关键字或者ReentrantLock等互斥锁机制虽然能够有效防止资源的竞争冲突但也可能带来额外的性能开销如上下文切换、锁竞争导致的线程阻塞等。而此时就出现了一种乐观锁的策略以其非阻塞、轻量级的特点在某些场合下能更好地提升并发性能其中最为关键的技术便是Compare And Swap简称CAS。 关于synchronize的实现原理请看移步这篇文章美团一面说说synchronized的实现原理问麻了。。。。 关于synchronize的锁升级请移步这篇文章京东二面Sychronized的锁升级过程是怎样的 CAS是一种无锁算法它在硬件级别提供了原子性的条件更新操作允许线程在不加锁的情况下实现对共享变量的修改。在Java中CAS机制被广泛应用于java.util.concurrent.atomic包下的原子类以及高级并发工具类如AbstractQueuedSynchronizerAQS的实现中。
CAS的基本概念与原理
CAS是一种原子指令常用于多线程环境中的无锁算法。CAS操作包含三个基本操作数内存位置、期望值和新值。在执行CAS操作时计算机会检查内存位置当前是否存放着期望值如果是则将内存位置的值更新为新值若不是则不做任何修改保持原有值不变并返回当前内存位置的实际值。
在Java中CAS机制被封装在jdk.internal.misc.Unsafe类中尽管这个类并不建议在普通应用程序中直接使用但它是构建更高层次并发工具的基础例如java.util.concurrent.atomic包下的原子类如AtomicInteger、AtomicLong等。这些原子类通过JNI调用底层硬件提供的CAS指令从而在Java层面上实现了无锁并发操作。 这里指的注意的是在JDK1.9之前CAS机制被封装在sun.misc.Unsafe类中在JDK1.9之后就使用了 jdk.internal.misc.Unsafe。这点由java.util.concurrent.atomic包下的原子类可以看出来。而sun.misc.Unsafe被许多第三方库所使用。 CAS实现原理
在Java中虽然Java语言本身并未直接提供CAS这样的原子指令但是Java可以通过JNI调用本地方法来利用硬件级别的原子指令实现CAS操作。在Java的标准库中特别是jdk.internal.misc.Unsafe类提供了一系列compareAndSwapXXX方法这些方法底层确实是通过C编写的内联汇编来调用对应CPU架构的cmpxchg指令从而实现原子性的比较和交换操作。
cmpxchg指令是多数现代CPU支持的原子指令它能在多线程环境下确保一次比较和交换操作的原子性有效解决了多线程环境下数据竞争的问题避免了数据不一致的情况。例如在更新一个共享变量时如果期望值与当前值相匹配则原子性地更新为新值否则不进行更新操作这样就能在无锁的情况下实现对共享资源的安全访问。 我们以java.util.concurrent.atomic包下的AtomicInteger为例分析其compareAndSet方法。
public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID 6214790243416807050L;//由这里可以看出来依赖jdk.internal.misc.Unsafe实现的private static final jdk.internal.misc.Unsafe U jdk.internal.misc.Unsafe.getUnsafe();private static final long VALUE U.objectFieldOffset(AtomicInteger.class, value);private volatile int value;public final boolean compareAndSet(int expectedValue, int newValue) { // 调用 jdk.internal.misc.Unsafe的compareAndSetInt方法return U.compareAndSetInt(this, VALUE, expectedValue, newValue); }
}Unsafe中的compareAndSetInt使用了HotSpotIntrinsicCandidate注解修饰HotSpotIntrinsicCandidate注解是Java HotSpot虚拟机JVM的一个特性注解它表明标注的方法有可能会被HotSpot JVM识别为“内联候选”当JVM发现有方法被标记为内联候选时会尝试利用底层硬件提供的原子指令比如cmpxchg指令直接替换掉原本的Java方法调用从而在运行时获得更好的性能。
public final class Unsafe {HotSpotIntrinsicCandidate public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
} compareAndSetInt这个方法我们可以从openjdk的hotspot源码位置hotspot/src/share/vm/prims/unsafe.cpp中可以找到
{CC compareAndSetObject,CC ( OBJ J OBJ OBJ )Z, FN_PTR(Unsafe_CompareAndSetObject)},{CC compareAndSetInt, CC ( OBJ JII)Z, FN_PTR(Unsafe_CompareAndSetInt)},{CC compareAndSetLong, CC ( OBJ JJJ)Z, FN_PTR(Unsafe_CompareAndSetLong)},{CC compareAndExchangeObject, CC ( OBJ J OBJ OBJ ) OBJ, FN_PTR(Unsafe_CompareAndExchangeObject)},{CC compareAndExchangeInt, CC ( OBJ JII)I, FN_PTR(Unsafe_CompareAndExchangeInt)},{CC compareAndExchangeLong, CC ( OBJ JJJ)J, FN_PTR(Unsafe_CompareAndExchangeLong)},关于openjdk的源码本文源码版本为1.9如需要该版本源码或者其他版本下载方法请关注本公众号【码农Academy】后后台回复【openjdk】获取 而hostspot中的Unsafe_CompareAndSetInt函数会统一调用Atomic的cmpxchg函数
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {oop p JNIHandles::resolve(obj);jint* addr (jint *)index_oop_from_field_offset_long(p, offset);
// 统一调用Atomic的cmpxchg函数
return (jint)(Atomic::cmpxchg(x, addr, e)) e;} UNSAFE_END而Atomic的cmpxchg函数源码(位置hotspot/src/share/vm/runtime/atomic.hpp)如下
/**
*这是按字节大小进行的cmpxchg操作的默认实现。它使用按整数大小进行的cmpxchg来模拟按字节大小进行的cmpxchg。不同的平台可以通过定义自己的内联定义以及定义VM_HAS_SPECIALIZED_CMPXCHG_BYTE来覆盖这个默认实现。这将导致使用特定于平台的实现而不是默认实现。
* exchange_value要交换的新值。
* dest指向目标字节的指针。
* compare_value要比较的值。
* order内存顺序。
*/
inline jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest,jbyte compare_value, cmpxchg_memory_order order) {STATIC_ASSERT(sizeof(jbyte) 1);volatile jint* dest_int static_castvolatile jint*(align_ptr_down(dest, sizeof(jint)));size_t offset pointer_delta(dest, dest_int, 1);// 获取当前整数大小的值并将其转换为字节数组。jint cur *dest_int;jbyte* cur_as_bytes reinterpret_castjbyte*(cur);// 设置当前整数中对应字节的值为compare_value。这确保了如果初始的整数值不是我们要找的值那么第一次的cmpxchg操作会失败。cur_as_bytes[offset] compare_value;// 在循环中不断尝试更新目标字节的值。do {// new_valjint new_value cur;// 复制当前整数值并设置其中对应字节的值为exchange_value。reinterpret_castjbyte*(new_value)[offset] exchange_value;// 尝试使用新的整数值替换目标整数。jint res cmpxchg(new_value, dest_int, cur, order);if (res cur) break; // 如果返回值与原始整数值相同说明操作成功。// 更新当前整数值为cmpxchg操作的结果。cur res;// 如果目标字节的值仍然是我们之前设置的值那么继续循环并再次尝试。} while (cur_as_bytes[offset] compare_value);// 返回更新后的字节值return cur_as_bytes[offset];
}而由cmpxchg函数中的do...while我们也可以看出当多个线程同时尝试更新同一内存位置且它们的期望值相同但只有一个线程能够成功更新时其他线程的CAS操作会失败。对于失败的线程常见的做法是采用自旋锁的形式即循环重试直到成功为止。这种方式在低竞争或短时间窗口内的并发更新时相比于传统的锁机制它避免了线程的阻塞和唤醒带来的开销所以它的性能会更优。
Java中的CAS实现与API
在Java中CAS操作的实现主要依赖于两个关键组件sun.misc.Unsafe类、jdk.internal.misc.Unsafe类以及java.util.concurrent.atomic包下的原子类。尽管Unsafe类提供了对底层硬件原子操作的直接访问但由于其API是非公开且不稳定的所以在常规开发中并不推荐直接使用。Java标准库提供了丰富的原子类它们是基于Unsafe封装的安全、便捷的CAS操作实现。
java.util.concurrent.atomic包
Java标准库中的atomic包为开发者提供了许多原子类如AtomicInteger、AtomicLong、AtomicReference等它们均内置了CAS操作逻辑使得我们可以在更高的抽象层级上进行无锁并发编程。 原子类中常见的CAS操作API包括
compareAndSet(expectedValue, newValue)尝试将当前值与期望值进行比较如果一致则将值更新为新值返回是否更新成功的布尔值。getAndAdd(delta)原子性地将当前值加上指定的delta值并返回更新前的原始值。getAndSet(newValue)原子性地将当前值设置为新值并返回更新前的原始值。
这些方法都是基于CAS原理能够在多线程环境下保证对变量的原子性修改从而在不引入锁的情况下实现高效的并发控制。
CAS的优缺点与适用场景
CAS摒弃了传统的锁机制避免了因获取和释放锁产生的上下文切换和线程阻塞从而显著提升了系统的并发性能。并且由于CAS操作是基于硬件层面的原子性保证所以它不会出现死锁问题这对于复杂并发场景下的程序设计特别重要。另外CAS策略下线程在无法成功更新变量时不需要挂起和唤醒只需通过简单的循环重试即可。
但是在高并发条件下频繁的CAS操作可能导致大量的自旋重试消耗大量的CPU资源。尤其是在竞争激烈的场景中线程可能花费大量的时间在不断地尝试更新变量而不是做有用的工作。这个由刚才cmpxchg函数可以看出。对于这个问题我们可以参考synchronize中轻量级锁经过自旋超过一定阈值后升级为重量级锁的原理我们也可以给自旋设置一个次数如果超过这个次数就把线程挂起或者执行失败。(自适应自旋)
另外Java中的原子类也提供了解决办法比如LongAdder以及DoubleAdder等LongAdder过分散竞争点来减少自旋锁的冲突。它并没有像AtomicLong那样维护一个单一的共享变量而是维护了一个Base值和一组Cell桶结构。每个Cell本质上也是一个可以进行原子操作的计数器多个线程可以分别在一个独立的Cell上进行累加只有在必要时才将各个Cell的值汇总到Base中。这样一来大部分时候线程间的修改不再是集中在同一个变量上从而降低了竞争强度提高了并发性能。 ABA问题 单纯的CAS无法识别一个值被多次修改后又恢复原值的情况可能导致错误的判断。比如现在有三个线程 即线程1将str从A改成了B然后线程3将str又从B改成了A而此时对于线程2来说他就觉得这个值还是A所以就不会在更改了。
而对于这个问题其实也很好解决我们给这个数据加上一个时间戳或者版本号乐观锁概念。即每次不仅比较值还会比较版本。比如上述示例初始时str的值的版本是1然后线程2操作后值变成B而对应版本变成了2然后线程3操作后值变成了A版本变成了3而对于线程2来说虽然值还是A但是版本号变了所以线程2依然会执行替换的操作。
Java的原子类就提供了类似的实现如AtomicStampedReference和AtomicMarkableReference引入了附加的标记位或版本号以便区分不同的修改序列。 总结
Java中的CAS原理及其在并发编程中的应用是一项非常重要的技术。CAS利用CPU硬件提供的原子指令实现了在无锁环境下的高效并发控制避免了传统锁机制带来的上下文切换和线程阻塞开销。Java通过JNI接口调用底层的CAS指令封装在jdk.internal.misc类和java.util.concurrent.atomic包下的原子类中为我们提供了简洁易用的API来实现无锁编程。
CAS在带来并发性能提升的同时也可能引发循环开销过大、ABA问题等问题。针对这些问题Java提供了如LongAdder、AtomicStampedReference和AtomicMarkableReference等工具类来解决ABA问题同时也通过自适应自旋、适时放弃自旋转而进入阻塞等待等方式降低循环开销。
理解和熟练掌握CAS原理及其在Java中的应用有助于我们在开发高性能并发程序时作出更明智的选择既能提高系统并发性能又能保证数据的正确性和一致性。
本文已收录于我的个人博客码农Academy的博客专注分享Java技术干货包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等