单例模式最佳实践 2020-09-10

单例模式
 
维基百科:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance. This is useful when exactly one object is needed to coordinate actions across the system. The term comes from the mathematical concept of a singleton.
 
说人话:
让一个类在系统中永远最多有一个实例存在。
 
从定义上看,这似乎是一个非常简单的设计模式,但是当涉及到实现时,它会带来很多实现方面的问题。
 
特点
  • 单例模式限制了类的实例化,并确保Java虚拟机中仅存在该类的一个实例。
  • 单例类必须提供全局访问点才能获取该类的实例。
 
场景
  • 用于日志记录,驱动程序对象,缓存和线程池。
  • 还用于其他设计模式,例如 Abstract Factory,Builder,Prototype,Facade等。
  • 还在Java核心类中使用,例如:java.lang.Runtime,java.awt.Desktop。
  • 还在Spring框架中使用,比如Spring中的bean的创建。
 
实现
 
为了实现Singleton模式,有不同的方法,但是所有方法都具有以下共同的思路。
 
  • 私有构造函数,用于限制该类在别的类中实例化。
  • 同一个单例类的私有静态变量,是该类的唯一实例(对象)。
  • 返回类实例的公共静态方法,是外部获取单例类实例的唯一全局访问点。
 
1、饿汉(Eager initialization)
饿汉方式在 类加载时立即创建单例类的实例,这是最简单的方法。但是,缺点是几十客户端应用程序可能不是用它,该实例 也会被创建
public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();
    
    // 私有构造方法保证了无法被私自创建实例
    private EagerInitializedSingleton() {}
 
    public static EagerInitializedSingleton getInstance() {
        return INSTANCE;
    }
}
如果你的单例类没有使用很多资源,则可以用这种方法。
但是大多情况下,都是为文件系统、数据库连接等资源创建单例类的。
另外,它不能处理Excepton。
 
2、静态代码块(Static block initialization)
静态代码块初始化与饿汉类似,不同的是,实例化单例类时可以对Exception进行处理。
public class StaticBlockSingleton {
 
    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton() {}
    
    // 可以在静态代码块中处理异常
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch(Exception e) {
            throw new RuntimeException(“单例对象初始化异常");
        }
    }
    
    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}
饿汉与静态代码块的初始化方法都是在 使用实例之前就创建了实例,这显然不是最佳实践。
 
3、懒汉(Lazy Initialization)
懒汉初始化方法的特点是在 全局访问方法中创建实例。
public class LazyInitializedSingleton {
 
    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton() {}
    
    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}
上面的实现在单线程环境下可以很好地工作,但是对于 多线程系统,如果多个线程同时位于if条件中,则可能导致问题。它将 破坏单例模式,并且两个线程都将获得单例类的不同实例。下面,我们将介绍创建线程安全的单例类的不同方法。
 
4、线程安全的懒汉(Thread Safe Singleton)
实现一个线程安全的单例类,最简单的方式时使用全局访问的 同步方法,以便一次仅有一个线程可以执行此方法。
public class ThreadSafeSingleton {
 
    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton() {}
    
    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}
这种实现可以很好的工作,并且事线程安全的。但是由于同步方法存在一定的成本,所以它降低了性能,尽管我们可能只是在创建实例的前几个线程中需要这种同步锁。
所以为了避免每次的额外开销,我们可以使用双重检查的锁定原理。
 
Talk is cheap. Show me the code. 
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}
但是,这种方式在高并发情况下可能 会失败
 
会失败?
请看: instance = new ThreadSafeSingleton(); 
这行代码不是一个 原子操作,实际上在JVM中大概做了三件事:
    ①给instance分配内存
    ② 调用构造方法完成实例的初始化(写入堆内存)
    ③让实例对象instance变量指向①的内存地址(完成这一步instance就不是null了)
 
在JVM即时编译器中存在 指令重排序,它会对语句执行优化,只要语句间没有逻辑依赖关系,那它就有可能并且有权对其进行优化。
在这里要说下as-is-serial这个东西,as-if-serial是指在执行结果不会改变的情况下,JVM为了提高程序的执行效率会对指令进行重排序。
 
就像上面的程序, ②是依赖于①的,所以为了保证程序的正确性,②在①之后执行,但是③和②不存在依赖性,是③和②谁先执行都不影响结果,所以JVM就可能会对它们进行重排序。也就是执行顺序可能是:①->③-> ②
如果在 ③已经执行完了,但是 ②还没有完成初始化时,这时候来了一个线程,当这个线程执行到 if(instance == null)  时,它发现不为null,则立即 return instance 了。
问题来了,如果这时要用到这个instance,但还没初始化完成,可能就会出现意料之外的事情。
 
那有 解决办法 吗?
有的。我们可以通过 volatile关键字来修饰instance,它可以 阻止指令重排序,在 volatile 变量的赋值操作后面会有一个 内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前,从而保证初始化的有序性。
private static volatile ThreadSafeSingleton instance;
 
万无一失了吗?
Java 5 以前的版本使用了 volatile 的双重检查锁还是有问题的。其原因是 Java 5 以前的 JMM(Java 内存模型)是 存在缺陷的,既是将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
 
Tips:不得不说这种方式太多地方要注意了,很不友好,所以我们再换个路子吧!
 
5、静态 内部 类( Bill Pugh Singleton Implementation)
冷知识: Bill Pugh 是个歪果仁,是他想到的这种方法。
public class BillPughSingleton {
 
    private BillPughSingleton() {}
    
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
私有的静态内部类中包含单例类的实例。加载 singleton类时,SingletonHelper 类不会夹在到内存中,在调用 getInstance 方法时类才会加载到内存中并创建类的实例。
这是一种比较广泛的实现方法,因为它不需要同步。
 
6、枚举(Enum Singleton)
Joshua Bloch 建议使用Enum来实现Singleton设计模式,因为Java确保在Java程序中仅将一次枚举值实例化一次。由于Java枚举值可全局访问,因此单例也是如此。缺点是枚举类型有些不灵活;例如,它不允许延迟初始化。
public enum EnumSingleton {
 
    INSTANCE;
    
    public static void doSomething(){
        // 该干嘛干嘛
    }
}
 
关于这位大哥为什么建议用枚举,后面说到“破坏者”时会提到。
 
 
破坏者
 
1、 反射来也!bububu……
反射 可以销毁上述除枚举之外的所有单例实现方法。让我们用一个示例类来看看。
import java.lang.reflect.Constructor;
 
public class ReflectionSingletonTest {
 
    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // 下面的代码会毁掉单例模式
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}
 
运行上述测试类时,会注意到两个实例的hashCode会不相同,这会破坏单例模式。反射非常强大,并在例如Spring和Hibernate的许多框架中使用。
 
2、 序列化来搞事!ada……
有时候我们需要在Singleton类中实现Serializable接口,以便我们可以将其状态存储在文件系统中,并在以后的某个时间使用它。
请看一个例子。
import java.io.Serializable;
 
public class SerializedSingleton implements Serializable {
 
    private static final long serialVersionUID = -7604766932017737115L;
 
    private SerializedSingleton() {}
    
    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }
    
    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }
}
 
序列化一个单例类的问题在于,每当我们反序列化它时,它将创建该类的新实例。让我们用一个简单的程序看看它。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
 
public class SingletonSerializedTest {
 
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
 
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();
        
        // 将文件反序列化为对象
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();
        
        System.out.println("instanceOne hashCode=“ + instanceOne.hashCode());
        System.out.println("instanceTwo hashCode=“ + instanceTwo.hashCode());
    }
}
 
上面程序的输出是:
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522
因此,它破坏了单例模式,要克服这种情况,我们需要做的工作是为单例类提供 readResolve() 方法的实现。
 
// 反序列化时有这个方法,就会使用这个方法的返回值来作为对象
protected Object readResolve() throws ObjectStreamException {
    System.out.println("readResolve() invoked.");
    return getInstance();
}
这样的话上述程序的运行结果就会是:两个对象的 hashCode 相同。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值