共计 3663 个字符,预计需要花费 10 分钟才能阅读完成。
单例模式
单例模式是 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()的形式调用即可。
枚举如何实现线程平安?反编译后能够发现,会通过一个类去继承改枚举,而后通过动态代码块的形式在类加载时实例化对象,与饿汉式相似。https://blog.csdn.net/wufalia…
如何做到避免反序列化调用?每一个枚举类型及其定义的枚举变量在 JVM 中都是惟一的,Java 做了非凡的规定,枚举类型序列化和反序列化进去的是同一个对象。
除此之外,枚举还能够避免反射调用。
** 综上,线程平安的几种单例模式比拟来看:
枚举(无锁,调用效率高,能够避免反射和反序列化调用,不能延时加载)> 动态外部类(无锁,调用效率高,能够延时加载)> 双重同步锁(有锁,调用效率高于懒汉式,能够延时加载)> 懒汉式(有锁,调用效率不高,能够延时加载)≈ 饿汉式(无锁,调用效率高,不能延时加载)
ps: 只有枚举能避免反射和反序列化调用 **