单例模式

单例模式是 Java 中常用的设计模式之一,属于设计模式三大类中的创建型模式。在运行期间,保证某个类仅有一个实例,并提供一个访问它的全局访问点。单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。实现线程安全的单例模式有以下几种方式:

1.饿汉式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

}

这是实现一个安全的单例模式的最简单粗暴的写法,之所以称之为饿汉式,是因为肚子饿了,想马上吃到东西,不想等待生产时间。在类被加载的时候就把Singleton实例给创建出来供使用,以后不再改变

优点:实现简单, 线程安全,调用效率高(无锁,且对象在类加载时就已创建,可直接使用)。

缺点:可能在还不需要此实例的时候就已经把实例创建出来了,不能延时加载(在需要的时候才创建对象)。

2.懒汉式

public class Singleton {
    
    private static Singleton instance = null;

    private Singleton() {
    }

    //如果没有synchronized,则线程不安全
    public static synchronized Singleton getInstance() {//synchronized也可以写在方法里,形成同步代码块
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
}

相比饿汉式,懒汉式显得没那么“饿”,在真正需要的时候再去创建实例

优点:线程安全,可以延时加载。

缺点:调用效率不高(有锁,且需要先创建对象)。

3.懒汉式改良版(双重同步锁)

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

使用了double-check,即check-加锁-check,减少了同步的开销

第2种懒汉式的效率低在哪里呢?第二种写法将synchronized加在了方法上(或者写在方法里),在单例对象被创建后,因为方法加了锁,所以要等当前线程得到对象释放锁后,下一个线程才可以进入getInstance()方法获取对象,也就是线程要一个一个的去获取对象。而采用双重同步锁,在synchronized代码块前加了一层判断,这使得在对象被创建之后,多线程不需进入synchronized代码块中,可以多线程同时并发访问获取对象,这样效率大大提高。

在创建第一个对象时候,可能会有线程1,线程2两个线程进入getInstance()方法,这时对象还未被创建,所以都通过第一层check。接下来的synchronized锁只有一个线程可以进入,假设线程1进入,线程2等待。线程1进入后,由于对象还未被创建,所以通过第二层check并创建好对象,由于对象singleton是被volatile修饰的,所以在对singleton修改后会立即将singleton的值从其工作内存刷回到主内存以保证其它线程的可见性。线程1结束后线程2进入synchronized代码块,由于线程1已经创建好对象并将对象值刷回到主内存,所以这时线程2看到的singleton对象不再为空,因此通过第二层check,最后获取到对象。这里volatile的作用是保证可见性,同时也禁止指令重排序,因为上述代码中存在控制依赖,多线程中对控制依赖进行指令重排序会导致线程不安全。

优点:线程安全,可以延时加载,调用效率比2高。

4.内部静态类

public class Singleton {

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonFactory.instance;
    }

    private static class SingletonFactory {
        private static Singleton instance = new Singleton();
    }

}

静态内部类只有被主动调用的时候,JVM才会去加载这个静态内部类。外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。

优点:线程安全,调用效率高,可以延时加载。

 

似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击和反序列化攻击

a)反射攻击

public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);
}

运行结果:false

通过结果看,这两个实例不是同一个,违背了单例模式的原则。

b)反序列化攻击

引入依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

这个依赖提供了序列化和反序列化工具类。

Singleton类实现java.io.Serializable接口。

public class Singleton implements Serializable {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {

    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }

}

运行结果:false

 

5.枚举

最佳的单例实现模式就是枚举模式。写法简单,线程安全,调用效率高,可以天然的防止反射和反序列化调用,不能延时加载。

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}

调用方法:

public class Main {

    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }

}

直接通过Singleton.INSTANCE.doSomething()的方式调用即可。

枚举如何实现线程安全?反编译后可以发现,会通过一个类去继承改枚举,然后通过静态代码块的方式在类加载时实例化对象,与饿汉式类似。

如何做到防止反序列化调用?每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,Java做了特殊的规定,枚举类型序列化和反序列化出来的是同一个对象。

除此之外,枚举还可以防止反射调用。

 

综上,线程安全的几种单例模式比较来看:

枚举(无锁,调用效率高,可以防止反射和反序列化调用,不能延时加载)> 静态内部类(无锁,调用效率高,可以延时加载) > 双重同步锁(有锁,调用效率高于懒汉式,可以延时加载) > 懒汉式(有锁,调用效率不高,可以延时加载) ≈ 饿汉式(无锁,调用效率高,不能延时加载)

ps:只有枚举能防止反射和反序列化调用