在多线程并发编程中,多个线程同时修改同一个对象的字段时,很容易出现数据不一致的问题。传统的解决方案是使用synchronized关键字加锁,但加锁会带来线程阻塞、上下文切换等性能开销。VarHandle是Java 9引入的底层工具,能够直接对对象的字段进行原子性操作,支持CAS(比较并交换)等无锁化操作,既保证了操作的原子性,又避免了锁带来的性能损耗。

VarHandle基础概念
VarHandle是java.lang.invoke包下的一个类,它可以看作是对某个变量(字段、数组元素等)的强类型引用,提供了对变量进行原子性、有序性操作的能力。和传统的反射操作相比,VarHandle的性能更高,因为它在初始化时会进行访问检查,后续操作不需要重复检查,而且很多操作会被JVM优化为底层机器指令。
要使用VarHandle操作某个字段,首先需要获取该字段对应的VarHandle实例,获取方式是通过MethodHandles.Lookup类来实现,示例代码如下:
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandleDemo {
// 待操作的字段,需要用volatile修饰保证可见性
private volatile int count = 0;
private static final VarHandle COUNT_HANDLE;
static {
try {
// 获取当前类的Lookup对象
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 获取count字段对应的VarHandle,参数分别是:字段所在类、字段类型、字段名
COUNT_HANDLE = lookup.findVarHandle(VarHandleDemo.class, "count", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
}
使用VarHandle实现CAS操作
CAS操作的核心逻辑是:先比较变量的当前值是否等于预期值,如果相等,就将变量更新为新值,否则不做任何操作,整个过程是原子性的。VarHandle提供了compareAndSet方法来实现CAS操作,方法签名为:
boolean compareAndSet(Object... args)
对于对象字段的VarHandle,compareAndSet的第一个参数是对象实例,第二个参数是预期值,第三个参数是新值,返回true表示更新成功,返回false表示更新失败。
下面是一个使用VarHandle实现无锁计数器递增的示例,多个线程同时调用increment方法,不会出现计数错误:
public class LockFreeCounter {
private volatile int count = 0;
private static final VarHandle COUNT_HANDLE;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
COUNT_HANDLE = lookup.findVarHandle(LockFreeCounter.class, "count", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
// 无锁递增方法
public void increment() {
int oldValue;
int newValue;
do {
// 获取当前count的值
oldValue = (int) COUNT_HANDLE.get(this);
// 计算新值
newValue = oldValue + 1;
// 循环尝试CAS操作,直到成功
} while (!COUNT_HANDLE.compareAndSet(this, oldValue, newValue));
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
LockFreeCounter counter = new LockFreeCounter();
// 创建10个线程,每个线程递增1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完成
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数,结果应该是10000
System.out.println("最终计数:" + counter.getCount());
}
}
VarHandle的其他常用原子操作
除了compareAndSet,VarHandle还提供了很多其他原子操作方法,常用的包括:
- getAndSet:原子性地设置变量为新值,返回旧值
- getAndAdd:原子性地将变量增加指定值,返回旧值
- addAndGet:原子性地将变量增加指定值,返回新值
- weakCompareAndSet:弱版本的CAS操作,不保证和volatile读写的顺序性,性能可能更高,但使用场景更受限
下面是getAndAdd的使用示例:
public class VarHandleAddDemo {
private volatile int value = 0;
private static final VarHandle VALUE_HANDLE;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
VALUE_HANDLE = lookup.findVarHandle(VarHandleAddDemo.class, "value", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
public int add(int delta) {
// 原子性增加delta,返回增加前的值
return (int) VALUE_HANDLE.getAndAdd(this, delta);
}
public int getValue() {
return value;
}
public static void main(String[] args) {
VarHandleAddDemo demo = new VarHandleAddDemo();
int old = demo.add(5);
System.out.println("旧值:" + old); // 输出0
System.out.println("新值:" + demo.getValue()); // 输出5
}
}
VarHandle和传统原子类的对比
Java之前提供了AtomicInteger、AtomicLong等原子类来实现无锁原子操作,这些原子类底层也是基于CAS实现的,而VarHandle可以看作是原子类的底层实现工具。两者的差异如下:
| 对比项 | VarHandle | 传统原子类(如AtomicInteger) |
|---|---|---|
| 操作对象 | 可以操作任意对象的任意字段、数组元素 | 只能操作原子类内部封装的单个变量 |
| 灵活性 | 更高,支持更多类型的原子操作 | 较低,只提供封装好的固定操作 |
| 使用复杂度 | 略高,需要手动获取VarHandle实例 | 低,开箱即用 |
| 性能 | 更优,减少了额外的对象封装开销 | 略低,有对象封装开销 |
使用注意事项
在使用VarHandle进行原子性字段操作时,需要注意以下几点:
- 被操作的字段需要用volatile修饰,保证变量的可见性和禁止指令重排序,否则CAS操作可能无法获取到最新的值
- 获取VarHandle的过程比较耗时,而且会进行访问权限检查,因此通常将VarHandle定义为静态常量,在类初始化时完成获取
- CAS操作在高竞争场景下可能会出现大量自旋重试,导致CPU占用升高,此时需要评估是否适合使用无锁方案
- VarHandle的操作都是强类型的,如果传入的参数类型和字段类型不匹配,会抛出类型转换异常
通过VarHandle实现无锁化的CAS并发编程,能够在保证线程安全的前提下,有效提升并发场景下的程序性能,尤其适合字段操作频繁、竞争不是特别激烈的场景。开发者可以根据实际需求,选择合适的原子操作方法,实现高效的并发逻辑。