在 java 的 concurrenthashmap 中,volatile 关键字主要用于保证哈希桶数组(table)和节点(node)引用的可见性。具体做法是声明数组字段为 volatile node,并在初始化或扩容时通过赋值操作触发内存屏障,确保其他线程能立即看到最新的结构引用。

ConcurrentHashMap 如何保证多线程下的结构可见性
它通过 volatile 修饰的 table 数组实现。当某个线程执行 initTable() 或扩容方法时,会重新赋值 volatile 变量,这会将新创建的数组对象及其内部节点的引用刷新到主内存,从而让所有其他线程读取该变量时获取最新状态。

import java.util.concurrent.ConcurrentHashMap;
public class VolatileDemo {
public static void main(String[] args) {
ConcurrentHashMap<string string> map = new ConcurrentHashMap();
// 初始状态下,table 被 volatile 修饰
// 线程 A 调用 map.put("key", "value")
// 如果 table 为空,线程 A 会初始化 table 并赋值给 volatile 变量
// 线程 B 调用 map.get("key")
// 线程 B 读取 volatile table,确保看到线程 A 初始化的结构
map.put("key", "value");
System.out.println(map.get("key"));
}
}</string>
上述代码展示了基本用法。核心在于 put 和 get 操作前都会读取 volatile 的 table。由于 volatile 的可见性语义,线程 A 对 table 的写操作(包括数组对象的创建和填充)对线程 B 的读操作立即可见。这解决了多线程环境下,一个线程修改了哈希表结构,另一个线程可能因为 CPU 缓存导致看不到最新结构的“脏读”问题。
为什么 Node 的值也需要 volatile
除了数组本身,ConcurrentHashMap 中的 Node 类里的 value 和 next 指针也被声明为 volatile。这是为了保证节点内部数据的可见性。当线程更新某个键值对时,直接修改 Node.value,其他线程读取该 Node 的 value 时,能通过 volatile 保证读到最新值,而不需要加锁。

class SimpleNode<k v> {
final K key;
volatile V value;
volatile SimpleNode<k v> next;
SimpleNode(K key, V value, SimpleNode<k v> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
public class NodeVolatileDemo {
public static void main(String[] args) {
SimpleNode<string integer> node = new SimpleNode("k", 1, null);
// 线程 A 更新 value
node.value = 2;
// 线程 B 读取 value
// 由于 value 是 volatile,线程 B 一定能读到 2
System.out.println(node.value);
}
}</string></k></k></k>
这里模拟了 Node 的结构。在实际的 ConcurrentHashMap 源码中,ValVar 工具类提供了 putObjectVolatile 等方法来原子性地更新这些 volatile 字段。这种设计避免了在整个链表或树上进行全局锁竞争,只需对单个节点的引用或值进行可见性同步,极大地提高了并发性能。
基于 tabAt 和 setTabAt 的具体操作结果
在使用 Unsafe 类直接操作内存时,ConcurrentHashMap 使用 tabAt(table, i) 读取特定索引的节点,使用 setTabAt(table, i, node) 写入节点。这两个方法内部都调用了 volatile 的内存访问指令,确保了对数组元素操作的可见性和有序性。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeAccessDemo {
private static final Unsafe unsafe;
private static final long tableOffset;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
// 假设我们有一个简单的数组对象
Object[] arr = new Object[10];
tableOffset = unsafe.arrayBaseOffset(Object[].class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Object[] table = new Object[10];
// 模拟 setTabAt: 设置第 0 个元素
unsafe.putObjectVolatile(table, tableOffset, "value_at_0");
// 模拟 tabAt: 读取第 0 个元素
Object val = unsafe.getObjectVolatile(table, tableOffset);
System.out.println(val); // 输出: value_at_0
// 验证可见性:如果在另一个线程修改了 table[0]
// 当前线程通过 getObjectVolatile 也能立即看到变化
}
}
这段代码演示了底层机制。putObjectVolatile 和 getObjectVolatile 是 Unsafe 提供的原生方法,它们对应于 JVM 层面的 volatile 写和读。在 ConcurrentHashMap 中,即使数组本身不是 volatile 类型的对象(数组引用是),对数组元素的访问也通过这种方式保证了可见性。这意味着,当一个线程通过 setTabAt 将一个新节点放入数组的某个位置时,其他线程通过 tabAt 读取该位置时,一定能看到这个新节点,而不会看到旧的空值或过期数据。这是 ConcurrentHashMap 实现无锁化并发控制的关键基石之一。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











