1.背景
在计算机中,CAS(比较并交换)是一种并发控制机制,用于解决多个线程同时访问共享资源时可能出现的并发问题,其中最常见的问题是竞态条件。
竞态条件是指多个线程对同一资源进行读写操作时,最终结果的正确性取决于线程之间的执行顺序。如果线程之间没有适当的同步机制来保证正确的执行顺序,就有可能导致数据的不一致性和错误的结果。
CAS机制通过比较内存地址上的值与一个预期值来判断是否发生了竞态条件,然后根据比较结果来决定是否更新内存地址上的值。这个操作是原子性的,即在一次操作中完成,不会被其他线程中断。这样就可以保证在多线程环境下对共享资源的读写操作的正确性。
2.多线程下i++导致的问题
在这之前,我们举一个例子,在没有防范的情况下会出现什么情况。
在多线程情况下,对同一个变量进行自增操作可能会出现竞态条件(race condition)的情况。
进行i++操作。
- 线程A读取变量I的当前值为1;
- 线程A进行自增操作,将I的值加1,结果为2;
- 线程B读取变量I的当前值为1;
- 线程B进行自增操作,将I的值加1,结果也为2;
- 线程A将自增后的值2写回变量I;
- 线程B将自增后的值2写回变量I;
在这种情况下,我们本希望的线程B的结果是I的值变为3,但因为竞态条件的存在,实际结果却只是2,导致结果不符合预期。
3.AtomicInteger
这是一段i++的操作。
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.get());
我们来看看AtomicInteger基本的变量。
其中的valueOffset是一个静态final long类型的常量,用于存储value字段在内存中的偏移量。
value字段是一个volatile int类型的变量,用于存储AtomicInteger对象的值。volatile关键字保证了对变量的可见性,在多线程编程中,当一个线程修改了 volatile
变量的值时,这个变化对其他线程是可见的。这是因为 volatile
会告诉编译器和处理器,对该变量的读写操作需要从内存中进行,而不是从缓存或寄存器中获取,从而确保了线程之间的内存可见性。
在类首次被加载的时候,还使用了Unsafe类以及反射机制来获取value字段在内存中的偏移量。Unsafe类是一个提供了访问底层内存操作的工具类,可以绕过Java的访问控制机制直接操作内存,如CAS操作等。在这里,通过调用Unsafe.getUnsafe()方法获取unsafe对象,并使用objectFieldOffset()方法获取value字段的偏移量。
我们再来看看自增操作的核心代码。
/**
* var1 this 对象本身
* var2 偏移量
* var4 1
**/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 拿到主存中的值
var5 = this.getIntVolatile(var1, var2);
// 就是拿到主存中的值然后与期望值进行对比,如果一样就进行更新 var5 + var4 实现i++
// 如果匹配失败就会一直自旋直到成功。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
4.缺点
4.1ABA问题。
ABA 问题指的是在执行 CAS 操作时,如果变量的值在操作之前为 A,之后又被改为了 B,最后又被改回 A,那么 CAS 操作可能会错误地认为变量的值没有被修改过。这种情况下,CAS 无法检测到变量值的中间变化,可能导致数据不一致或不可预期的行为。
为了解决这个问题,可以使用带版本号的 CAS 操作,例如 Java 中的 AtomicStampedReference
。
4.2循环开销。
CAS 是一个自旋操作,即它会一直尝试执行直到成功为止。在高并发环境下,如果 CAS 操作失败,线程会一直自旋尝试,这可能会导致额外的 CPU 开销和性能下降。
1.限制自旋次数:以在 CAS 操作失败时限制线程自旋的次数,如果达到了指定的自旋次数仍然没有成功,则放弃自旋,转而采用其他策略,如阻塞等待或者进行重试。
2.使用线程优先级:可以根据线程的优先级来决定自旋次数。对于优先级较高的线程,可以允许更多的自旋次数,而对于优先级较低的线程,则可以限制自旋次数,以保证优先级较高的线程能够更快地获取锁。
4.3 无法保证公平性
CAS 操作是一种乐观锁,它不会像悲观锁一样在获取锁时阻塞线程,而是不断尝试,因此无法保证线程获取锁的公平性。这可能导致某些线程长时间无法执行,从而影响整体性能。