指针碰撞是一种在内存规整情况下进行对象内存分配的高效方式,但在多线程环境下使用指针碰撞会引发并发问题。
并发问题产生的原因
在指针碰撞的内存分配机制中,存在一个分界指针用于区分已使用内存和未使用内存。当有新对象需要分配内存时,只需将这个指针向未使用内存的方向移动与对象大小相等的距离。
而指针的 读取-修改-回写 操作需要不是原子的。所以,在多线程环境下,多个线程可能会同时尝试分配内存,会同时操作这个分界指针。由于线程执行的不确定性,如果多个线程同时读取到了相同的指针位置,并且都基于这个位置进行内存分配,就会导致多个线程为不同的对象分配到同一块内存区域,从而引发内存分配错误和数据混乱等问题。
比如当前分界指针指向内存地址为 100 的位置,有两个线程 Thread A
和 Thread B
同时要分配大小为 10 的内存空间。这两个线程可能会同时读取到指针位置为 100,然后都将指针向后移动 10 个单位,这样就会导致两个线程都认为自己分配到了地址从 100 到 109 的内存区域(此时指针更新到 110
),实际上这就造成了内存分配的冲突。
常见的解决办法
1. CAS(Compare-And-Swap)
CAS 是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。在进行内存分配时,线程会先读取当前指针的值作为预期原值(A),然后尝试将指针的值更新为新的位置(B)。在更新之前,会比较指针当前的实际值(V)是否与预期原值(A)相等。如果相等,说明没有其他线程修改过指针的值,就可以安全地更新指针;如果不相等,说明有其他线程已经修改了指针,当前线程需要重新读取指针的值并再次尝试更新。
// 模拟内存分配
class MemoryAllocator {
private AtomicInteger pointer; // 使用 AtomicInteger 实现 CAS
public MemoryAllocator(int initialPointer) {
this.pointer = new AtomicInteger(initialPointer);
}
// 分配内存的方法
public int allocate(int size) {
while (true) {
int current = pointer.get(); // 获取当前指针位置
int next = current + size; // 计算新的指针位置
// 使用 CAS 尝试更新指针
if (pointer.compareAndSet(current, next)) {
return current; // 分配成功,返回分配的内存起始地址
}
// CAS 失败,说明有其他线程修改了指针,重试
}
}
}
// 测试
public class PointerCollisionTest {
public static void main(String[] args) {
MemoryAllocator allocator = new MemoryAllocator(100);
// 创建多个线程进行内存分配
Thread thread1 = new Thread(() -> {
int address = allocator.allocate(10);
System.out.println("Thread 1 allocated memory at begin address: " + address);
});
Thread thread2 = new Thread(() -> {
int address = allocator.allocate(20);
System.out.println("Thread 2 allocated memory at begin address: " + address);
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码中,AtomicInteger
类提供了 CAS 操作的支持,compareAndSet
方法会比较当前指针的值是否与预期值相等,如果相等则更新指针的值。
2. TLAB(Thread Local Allocation Buffer)
TLAB 是一种线程私有的内存分配缓冲区。每个线程在 Java 堆中预先分配一块小的内存区域作为自己的 TLAB,线程在分配对象时,首先会尝试在自己的 TLAB 中进行分配。由于 TLAB 是线程私有的,所以在 TLAB 内进行内存分配时不会产生并发问题。只有当 TLAB 空间不足时,线程才会去全局堆中进行分配,此时可能会使用 CAS 等方式来解决并发问题。
通过使用 TLAB,可以减少多线程之间的竞争,提高内存分配的效率。Java 虚拟机默认开启了 TLAB 机制,当然也可以通过 -XX:+UseTLAB
参数来显式启用。