单例模式
单例模式是 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:只有枚举能防止反射和反序列化调用