单例模式
保证一个类中只有一个实例:用来维护某些全局资源、控制某些共享资源的访问权限;
提供一个访问该实例的全局节点,允许从程序中的任何地方访问该实例,该实例不会被其他变量覆盖;
单例的生命周期贯穿整个应用的生命周期,应该及时关闭销毁资源,避免出现内存泄露问题;
实现
Java 中实现单例模式的步骤:
将单例类的构造方法设置为 私有,防止其他方能够实例化该类,从而破坏单例模式;
声明一个
public static方法用于返回单例对象;
Kotlin实现单例模式只需要用
object或者data object
饿汉式
在类加载的时候就完成对单例对象的初始化操作
优点:
线程安全(类加载就是线程安全的)
执行效率高(调用时无需进行判断以及创建对象)
缺点:
当应用没有使用到该单例时,该单例一直存在于内存中,会造成内存的浪费;
class Singleton {
private Singleton() {}
private final static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}普通懒汉式
只有在需要用到该单例时才创建单例并初始化
优点:
不会造成内存浪费,需要才创建;
缺点:
线程不安全(特指下面的代码)
因为判断和创建操作并不是原子操作,在多线程环境下,可能存在多个线程同时创建单例
线程A和线程B同时看到主存中的
instance为 null,那么就会同时创建一次单例,也就是说在不同线程中的工作内存中存在不同的实例(不符合单例模式了)同时线程将
instance刷新会主存的时机不确定,就可能导致最终主存中的instance是两个线程中其中的一个实例;
class Singleton {
private Singleton() {}
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
加锁懒汉式
要保证每个线程只有一个线程能够初始化单例,就需要进行加锁;
下面两种方法都保证同一时间只有一个线程能够创建实例;
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}当实例创建完后,每次访问还要加锁就会造成性能下降,因此按照思路可以改成这样:
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}上面的代码还是存在问题,当多个线程进入判断语句后,只有一个线程能够进入同步块,而该线程创建完后释放锁,其他线程就会进入该同步块,继续创建实例;
因此我们还需要在同步块再加上一次 if 判断,这也就是 双重检查锁
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}以为这样就结束了?NoNoNo;
Jvm 存在指令重排,在多线程下面可能会出现这种情况:线程B可能拿到的是一个没有调用过构造方法初始化过的实例对象;
虽然这种情况很少出现,但是出现了就导致未知的出错
这种情况就是因为指令重排造成的,如何防止指令重排呢? volatile 关键字就能防止修饰的变量出现指令重排
因此最终的代码如下:
class Singleton {
private Singleton() { }
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}内部类实现
通过内部类延迟加载的特性(本质上是饿汉式,主类加载时并不会加载内部类, 用到了才会加载),将单例的实例化推迟到要用的时候;
因为类加载是线程安全的,所以
instance的创建也是线程安全的;
class Singleton {
private Singleton () {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
关于内部类延迟加载可以使用下面的例子:
class Singleton {
private static class SingletonHolder {
static {
System.out.println("Instantiation");
}
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
public class Main {
public static void main(String[] args) {
System.out.println("before using singleton");
Singleton.getInstance();
}
}
// 运行结果为:
// before using singleton
// Instantiation枚举式单例
枚举类可以很好的实现单例模式(本质上是饿汉式单例模式,在语言层面实现线程安全)
枚举类保证多线程同时访问时,只有一个实例对象被创建,不会出现线程安全问题;
不能通过反射破坏单例(反射会检查类是否为枚举类,如果是则会抛出异常)
不能通过反序列化破坏单例(每个枚举对象都有唯一
name属性,序列化只会将name属性序列化;在反序列化过程中会通过该name属性返回当前已有的枚举对象)
例子代码:
enum Singleton {
INSTANCE;
private int counter = 0;
public int getAndIncrement() {
final int value = counter;
counter ++;
return value;
}
}使用方法:Singleton.INSTANCE.getAndIncrement()
容器式单例
当单例较多且这些单例大致上可以归为一类时,可以使用一个容器将这些单例存起来
class SingletonContainer {
private static final Map<String, Object> container = new HashMap<>();
private SingletonContainer() { }
public static Object getInstance(String packageName) {
if (container.containsKey(packageName)) {
synchronized (SingletonContainer.class) {
try {
Class<?> clazz = Class.forName(packageName);
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object object = constructor.newInstance();
container.put(packageName, object);
return object;
} catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException |
InstantiationException | IllegalAccessException e) {
return null;
}
}
}
return container.get(packageName);
}
}破坏单例的方式
反射破坏单例
通过反射能够将重新创建一个新的单例;
通过获取到单例类的构造方法,然后通过该构造方法获取即可;
try {
Class<?> clazz = Singleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object object = constructor.newInstance();
System.out.println(object);
System.out.println(object == Singleton.getInstance());
} catch (Exception e) {
//
}对于饿汉式,可以在构造方法中加个判断:
class Singleton {
private Singleton() {
if (instance != null) {
throw new RuntimeException("use `getInstance` instead.`");
}
}
private final static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}对于懒汉式,则无法使用上面这种方法避免;
序列化破坏单例
如果某些单例类支持 序列化,那么可以先序列化再反序列化,反序列化会重新创建新的对象;
如果构造方法中存在判断(如上面饿汉式的做法),反序列化会失败(因为反序列化也是调用反射执行)
try {
FileOutputStream outputStream = new FileOutputStream("Singleton.obj");
ObjectOutputStream out = new ObjectOutputStream(outputStream);
// 对象序列化到文件中
out.writeObject(SingletonDemo.getInstance());
out.flush();
out.close();
outputStream.close();
FileInputStream inputStream = new FileInputStream("Singleton.obj");
ObjectInputStream inp = new ObjectInputStream(inputStream);
// 文件中反序列化
SingletonDemo instance = (SingletonDemo) inp.readObject();
System.out.println(SingletonDemo.getInstance());
System.out.println(instance);
// 结果为: false
System.out.println(instance == SingletonDemo.getInstance());
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}对于实现了 Serializable 接口的解决方法:加上 readResolve() 方法,用此方法返回一个实例即可:
@Serial
private Object readResolve() {
return instance;
}这时候反序列化就是同一个对象了
因为底层会通过反射判断是不是会有 readResolve() 这个方法,如果有则直接反射调用方法;