一、饿汉模式

public class Test {
    private static  Test test = new Test();
    public static Test getObject(){
       
        return test;
    }
    private Test(){
        //私有化构造器避免被实例化
    }
​
}

饿汉式顾名思义就是应用程序很饿了,急需一个对象,所以当类被加载的时候直接就创建一个类的对象,使用时通过getObject()方法获取

饿汉模式是线程安全的

风险:饿汉模式由于类加载时候就创建一个对象,无论是否需要创建都创建出来了,那么如果这个程序不是“饿”的就会浪费空间

而懒汉模式只有用的时候才会创建所以会节约空间,但是每次想创建的时候都会询问是否创建过了,所以会浪费很多时间

饿汉模式已经可以满足绝大多数的应用场景

二、懒汉模式

public class Test {
    private static  Test test;
    public static Test getObject(){
        if(test==null){
            test = new Test();
        }
        return test;
    }
    private Test(){
        //私有化构造器避免被实例化
    }
​
}

懒汉模式使用懒加载的思想,当调用getObject方法获取单例时才会实例化对象优化饿汉模式内存浪费的问题

不过因为每次获取都需要判断是否存在所以懒汉模式线程是不安全,所以生产环境我们一般是不用懒汉模式的

三、双重检查锁

public class Test {
    private static volatile Test test;

    public Test getObject() {

        if (test == null) {
            synchronized (Test.class) {
                if (test == null) {
                    return new Test();
                }
            }
        }
        return test;
    }
    private Test(){
        //私有化构造器避免被实例化
    }
​
}

双重检查锁是基于懒汉模式的升级,既能保证线程安全又能实现懒加载,在创建前先用synchronized 将类对象锁住保证线程安全

如果不用Volatile会怎么样?

对象实际上创建对象要进过如下几个步骤:

  • 分配内存空间。
  • 调用构造器,初始化实例。
  • 返回地址给引用

但是可能发生指令重排序的,那有可能构造函数在对象初始化完成前就赋值完成了,在内存里面开辟了一片存储区域后直接返回内存的引用,这个时候还没真正的初始化完对象。

但是别的线程去判断instance!=null,直接拿去用了,其实这个对象是个半成品,那就有空指针异常了。

存在风险

但是双重检查锁存在风险,因为jvm优化执行速度在保障happen-before的原则下会产生指令重排序的情况,虽然可以通过volatile 关键字禁止指令重排序,但在jdk低版本并不能完全禁止,所以会出问题

四、枚举类

//Test.object获取对象
public enum Test {
    object;
    Test(){

    }
}

枚举其实没什么好说的,枚举是java的一个语法糖,实际上是一个继承了enum的final类。但是枚举是一个天然的单例模式,用法也非常简单,安全性也是最高的,因为其他方法虽然保证了应用程序方面的安全性,但是面对暴力反射构造创建对象还是没有办法的。当然,也可以通过构造器里抛异常来解决暴力反射,具体可以看看StandardCharsets这个类,他就是通过在构造器里抛异常来阻止暴力反射的。

枚举的缺点也很明显的,因为他没法实现懒加载,

五、静态内部类模式

//getObject()获取单例
public class Test {
    private static class objectClass{
        private static Test object = new Test();
    }
    public static Test getObject(){
        return objectClass.object;
    }
    private Test(){
        //私有化构造器避免被实例化
    }
​
}

静态内部类就是java实现单例的各种方法中相对最好的方法。只要构建一个静态内部类,并在内部类中构造单例对象。这样既保证了多线程情况下的安全性,同时也实现了懒加载。极力推荐大家使用这种方法

六、CAS法

该方法转自笔者一位好友https://ovvow.com/posts/singleton/

public class SingletonTest {
    private static AtomicReference<SingletonTest> CAS_INSTANCE = new AtomicReference<>();

    public static SingletonTest getCasInstance() {
        for (; ; ) {
            SingletonTest singleton = CAS_INSTANCE.get();
            if (singleton != null) {
                return singleton;
            }
            singleton = new SingletonTest();
            if (CAS_INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

优点:既可以懒加载又线程安全

缺点:虽然能保证能拿到对象是单例的,但并不能保证只实例化一个对象,如果这是一个比较“笨重”的对象,这样就会加重虚拟机内存的负担。


七、总结

绝大多数情况直接用饿汉模式足以,其余情况用静态内部类

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐