CAS非锁怎样实现单例的一个缺陷

发布时间:2021-12-18 13:35:53 作者:柒染
来源:亿速云 阅读:164
# CAS非锁怎样实现单例的一个缺陷

## 引言

在并发编程中,单例模式是最常用的设计模式之一。为了保证线程安全,传统的实现方式会使用`synchronized`关键字或`Lock`进行加锁。而基于CAS(Compare-And-Swap)的无锁实现因其高性能特性逐渐流行,但这种实现方式存在一个容易被忽视的缺陷——**指令重排序导致的初始化问题**。本文将深入分析这一缺陷的原理、表现及解决方案。

---

## 一、CAS无锁单例的典型实现

以下是基于CAS的经典无锁单例实现代码:

```java
public class CASSingleton {
    private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<>();
    
    private CASSingleton() {}
    
    public static CASSingleton getInstance() {
        for (;;) {
            CASSingleton instance = INSTANCE.get();
            if (instance != null) {
                return instance;
            }
            instance = new CASSingleton();
            if (INSTANCE.compareAndSet(null, instance)) {
                return instance;
            }
        }
    }
}

实现原理

  1. 通过AtomicReference保证原子性操作
  2. 自旋(for(;;))直到成功创建实例
  3. CAS操作确保只有一个线程能成功初始化

二、隐藏的缺陷:指令重排序

上述代码看似完美,但实际上存在一个可见性问题:由于JVM的指令重排序优化,对象的初始化可能未完成就被其他线程访问。

1. 对象构造过程分解

当执行new CASSingleton()时,JVM会隐式执行三个步骤: 1. 分配对象内存空间 2. 初始化对象(调用构造函数) 3. 将引用指向内存地址(写入主存)

2. 重排序风险

JVM可能将步骤2和步骤3重排序,导致其他线程获取到未初始化完成的对象。时序如下:

线程A 线程B
1. 分配内存
3. 引用指向内存(未初始化)
2. 检测到INSTANCE非null并返回
2. 执行构造函数初始化

三、缺陷验证实验

通过以下代码可以复现问题:

public class CASSingleton {
    private int value = 42; // 需要初始化的字段
    
    public static void main(String[] args) {
        new Thread(() -> {
            CASSingleton instance = getInstance();
            System.out.println(instance.value); // 可能输出0
        }).start();
    }
}

可能输出结果0(而非预期的42),证明对象未完成初始化就被访问。


四、解决方案

方案1:使用volatile禁止重排序(推荐)

private static volatile CASSingleton instance;

public static CASSingleton getInstance() {
    CASSingleton temp = instance;
    if (temp == null) {
        synchronized (CASSingleton.class) {
            temp = instance;
            if (temp == null) {
                temp = new CASSingleton();
                instance = temp;
            }
        }
    }
    return temp;
}

方案2:静态内部类(JVM保证线程安全)

public class HolderSingleton {
    private static class Holder {
        static final CASSingleton INSTANCE = new CASSingleton();
    }
    
    public static CASSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

方案3:利用AtomicReference的volatile特性

private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<>();

public static CASSingleton getInstance() {
    CASSingleton instance = INSTANCE.get();
    if (instance == null) {
        instance = new CASSingleton();
        if (INSTANCE.compareAndSet(null, instance)) {
            return instance;
        }
    }
    return INSTANCE.get(); // 始终通过AtomicReference获取最新值
}

五、各方案对比

方案 性能 代码复杂度 安全性
原始CAS实现 ★★★★☆ ★★☆☆☆ ★☆☆☆☆
volatile+DCL ★★★☆☆ ★★★☆☆ ★★★★★
静态内部类 ★★★★★ ★☆☆☆☆ ★★★★★
AtomicReference优化 ★★★★☆ ★★★☆☆ ★★★★☆

六、结论

  1. CAS无锁单例在极端情况下会出现对象未初始化的问题,这是由JVM指令重排序特性导致
  2. 生产环境推荐使用volatile+双重检查锁或静态内部类实现
  3. 无锁编程虽然性能优异,但需要深入理解内存模型和CPU指令特性

“并发编程的艺术在于,不仅要考虑代码的正确性,还要理解运行时环境的微妙特性。” —— Brian Goetz

”`

(全文约1100字,可根据具体排版调整)

推荐阅读:
  1. 单例的实现方式
  2. 双重校验锁实现单例模式(对象单例,线程安全)

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

cas java

上一篇:为什么Java中的Collection类都继承了抽象类还要实现抽象类的接口

下一篇:如何进行springboot配置templates直接访问的实现

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》