Java——原子变量和CAS

原子变量和CAS

什么是原子变量?为什么需要它们呢?我们从synchronized说起。介绍过Counter类,使用synchronized关键字保证原子更新操作,代码如下:

java 复制代码
public class Counter {
	private int count;
	public synchronized void incr(){
		count++;
	}
	public synchronized int getCount(){
		return count;
	}
}

对于count++这种操作来说,使用synchronized成本太高了,需要先获取锁,最后需要释放锁,获取不到锁的情况下需要等待,还会有线程的上下文切换,这些都需要成本。

对于这种情况,完全可以使用原子变量代替,Java并发包中的基本原子变量类型有以下几种。

  • AtomicBoolean:原子Boolean类型,常用来在程序中表示一个标志位。
  • AtomicInteger:原子Integer类型。
  • AtomicLong:原子Long类型,常用来在程序中生成唯一序列号。
  • AtomicReference:原子引用类型,用来以原子方式更新复杂类型。

限于篇幅,主要介绍AtomicInteger。除了这4个

类,还有一些其他类,如针对数组类型的类AtomicLongArray、AtomicReferenceArray,以及用于以原子方式更新对象中的字段的类,如AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater等。Java 8增加了几个类,在高并发统计汇总的场景中更为适合,包括LongAdder、LongAccumulator、Double-Adder和DoubleAccumulator,具体可参见API文档。

1、AtomicInteger

1.1、基本用法

AtomicInteger有两个构造方法:

java 复制代码
public AtomicInteger(int initialValue)
public AtomicInteger()

第一个构造方法给定了一个初始值,第二个构造方法的初始值为0。

可以直接获取或设置AtomicInteger中的值,方法是:

java 复制代码
public final int get()
public final void set(int newValue)

之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法,部分方法如下:

java 复制代码
//以原子方式获取旧值并设置新值
public final int getAndSet(int newValue)
//以原子方式获取旧值并给当前值加1
public final int getAndIncrement()
//以原子方式获取旧值并给当前值减1
public final int getAndDecrement()
//以原子方式获取旧值并给当前值加delta
public final int getAndAdd(int delta)
//以原子方式给当前值加1并获取新值
public final int incrementAndGet()
//以原子方式给当前值减1并获取新值
public final int decrementAndGet()
//以原子方式给当前值加delta并获取新值
public final int addAndGet(int delta)

这些方法的实现都依赖另一个public方法:

java 复制代码
public final boolean compareAndSet(int expect, int update)

compareAndSet是一个非常重要的方法,比较并设置,我们以后将简称为CAS。该方法有两个参数expect和update,以原子方式实现了如下功能:如果当前值等于expect,则更新为update,否则不更新,如果更新成功,返回true,否则返回false。

AtomicInteger可以在程序中用作一个计数器,多个线程并发更新,也总能实现正确性。我们看个例子,如代码所示。

java 复制代码
public class AtomicIntegerDemo {
    private static AtomicInteger counter = new AtomicInteger(0);

    static class Visitor extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            threads[i] = new Visitor();
            threads[i].start();
        }
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter.get());
    }
}

程序的输出总是正确的,为1000000。

1.2、基本原理和思维

AtomicInteger的使用方法是简单直接的,它是怎么实现的呢?它的主要内部成员是:

java 复制代码
private volatile int value;

注意:它的声明带有volatile,这是必需的,以保证内存可见性。

它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:

java 复制代码
public final int incrementAndGet() {
	for(; ; ) {
    	int current = get();
    	int next = current + 1;
    	if(compareAndSet(current, next))
            return next;
    }
}

代码主体是个死循环,先获取当前值current,计算期望的值next,然后调用CAS方法进行更新,如果更新没有成功,说明value被别的线程改了,则再去取最新值并尝试更新直到成功为止。

与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的,它假定更新很可能冲突,所以先获取锁,得到锁后才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,但使用CAS更新,也就是进行冲突检测,如果确实冲突了,那也没关系,继续尝试就好了。synchronized代表一种阻塞式算法,得不到锁的时候,进入锁等待队列,等待其他线程唤醒,有上下文切换开销。原子变量的更新逻辑是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换开销。对于大部分比较简单的操作,无论是在低并发还是高并发情况下,这种乐观非阻塞方式的性能都远高于悲观阻塞式方式。

原子变量相对比较简单,但对于复杂一些的数据结构和算法,非阻塞方式往往难于实现和理解,幸运的是,Java并发包中已经提供了一些非阻塞容器,我们只需要会使用就可以了,比如:

  • ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞并发队列。
  • ConcurrentSkipListMapConcurrentSkipListSet:非阻塞并发Map和Set。

但compareAndSet是怎么实现的呢?我们看代码:

java 复制代码
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

它调用了unsafe的compareAndSwapInt方法,unsafe是什么呢?它的类型为sun.misc. Unsafe,定义为:

java 复制代码
private static final Unsafe unsafe = Unsafe.getUnsafe();

它是Sun的私有实现,从名字看,表示的也是"不安全"​,一般应用程序不应该直接使用。原理上,一般的计算机系统都在硬件层次上直接支持CAS指令,而Java的实现都会利用这些特殊指令。从程序的角度看,可以将compareAndSet视为计算机的基本操作,直接接纳就好。

1.3、实现锁

基于CAS,除了可以实现乐观非阻塞算法之外,还可以实现悲观阻塞式算法,比如锁。实际上,Java并发包中的所有阻塞式工具、容器、算法也都是基于CAS的(不过,也需要一些别的支持)​。怎么实现锁呢?我们演示一个简单的例子,用AtomicInteger实现一个锁MyLock,如代码所示。

java 复制代码
public class MyLock {
    private AtomicInteger status = new AtomicInteger(0);
    public void lock() {
        while (!status.compareAndSet(0, 1)) {
            Thread.yield();
        }
    }
    
    public void unlock() {
        status.compareAndSet(1, 0);
    }
}

在MyLock中,使用status表示锁的状态,0表示未锁定,1表示锁定,lock()、unlock()使用CAS方法更新,lock()只有在更新成功后才退出,实现了阻塞的效果,不过一般而言,这种阻塞方式过于消耗CPU,我们后续章节介绍更为高效的方式。MyLock只是用于演示基本概念,实际开发中应该使用Java并发包中的类,如ReentrantLock。

2、ABA问题

使用CAS方式更新有一个ABA问题。该问题是指,假设当前值为A,如果另一个线程先将A修改成B,再修改回成A,当前线程的CAS操作无法分辨当前值发生过变化。

ABA是不是一个问题与程序的逻辑有关,一般不是问题。而如果确实有问题,解决方法是使用AtomicStampedReference,在修改值的同时附加一个时间戳,只有值和时间戳都相同才进行修改,其CAS方法声明为:

java 复制代码
public boolean compareAndSet(
    V expectedReference, V newReference, int expectedStamp, int newStamp)

比如:

java 复制代码
Pair pair = new Pair(100, 200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new
            AtomicStampedReference<Pair>(pair, stamp);
int newStamp = 2;
pairRef.compareAndSet(pair, new Pair(200, 200), stamp, newStamp);

AtomicStampedReference在compareAndSet中要同时修改两个值:一个是引用,另一个是时间戳。它怎么实现原子性呢?实际上,内部AtomicStampedReference会将两个值组合为一个对象,修改的是一个值,我们看代码:

java 复制代码
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
  Pair<V> current = pair;
  return
      expectedReference == current.reference &&
      expectedStamp == current.stamp &&
      ((newReference == current.reference &&
        newStamp == current.stamp) ||
        casPair(current, Pair.of(newReference, newStamp)));}

这个Pair是AtomicStampedReference的一个内部类,成员包括引用和时间戳,具体定义为:

java 复制代码
private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

AtomicStampedReference将对引用值和时间戳的组合比较和修改转换为了对这个内部类Pair单个值的比较和修改。

相关推荐
野生技术架构师1 小时前
2026最新Java面试八股文天花板(含详细解析)
java·jvm·spring
小碗羊肉2 小时前
【JavaWeb | 第十二篇】项目实战——登录功能
java·前端·数据库
喜欢小苹果的码农2 小时前
Java动态定时任务
java
haiyangyiba2 小时前
修改jar包中class的包路径
java·jar·修改class·修改class中包路径
雨落在了我的手上3 小时前
初识java(七):Java调试案例讲解
java·intellij-idea·集成开发环境调试功能
Volunteer Technology3 小时前
Spring AI MCP 案例-WebFlux SSE传输模式 (九)
java·数据库·人工智能·spring
rabbit_pro3 小时前
SpringBoot3集成Langchain4j使用Ollama
java·开发语言
解决问题no解决代码问题3 小时前
JAVA GC
java·开发语言·jvm
小程故事多_803 小时前
Agent Loop 核心突破,上下文压缩四大流派,重新定义窗口资源利用率
java·开发语言·人工智能