目录
前言
单例模式是保证任何情况下,都仅有一个实例,并提供全局访问的方法。
一、饿汉式
先上代码
/**
* 饿汉式
*/
public class Singleton1 {
private static final Singleton1 instance = new Singleton1();
/**
* 私有化构造方法 (防止手动new)
*/
private Singleton1(){}
public static Singleton1 getInstance(){
return instance;
}
}
饿汉式的关键在于instance作为类变量直接得到初始化,该方法能够百分之百的保证同步,也就是说instance在多线程下也不可能被实例化两次,但是instance被ClassLoader加载后可能很长时间才会被使用,那就意味着instance实例所开辟的空间的堆内存会驻留更久的时间。
如果一个类中的成员属性比较少,所占用的内存资源不多,饿汉式也未尝不可。总结起来,饿汉式可以保证多线程下唯一的实例,getInstance性能也比较高,但是无法进行懒加载
。
二、懒汉式
/**
* 懒汉式
*/
public class Singleton2 {
private static Singleton2 instance = null;
/**
* 私有化构造方法
*/
private Singleton2() {
}
// 存在线程安全问题
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
Singleton2 的类变量instance = null,当Singleton2.class被初始化的时候instance并不会被实例化,在getInstance方法中会判断instance 实例是否被实例化,看起来没什么问题,但在多线程环境下,会导致instance可能被实例化多次。 线程1判断null == instance
为true时,还没有实例化instance,切换到了线程2运行,线程2判断null == instance也为true。就会实例化多次。
三、懒汉式 + 同步
// 解决线程安全问题,但是效率低
public synchronized static Singleton2 getInstance2() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
采用懒汉式 + 数据同步方式既满足了懒加载又能百分之百保证instance实例的唯一性,但是synchronized 关键字天生的排他性导致了getInstance方法只能在同一时刻被一个线程所访问,性能低下。
四、懒汉式 + Double-Check
// 解决线程安全问题,提升效率
private static Singleton2 instance = null;
private String msg;
/**
* 私有化构造方法
*/
private Singleton2() {
msg = "初始化参数";
}
public static Singleton2 getInstance3() {
if (instance == null) {
synchronized (Singleton2.class){
if(instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
当两个线程发现null == instance
成立时,只有一个线程有资格进入同步代码块,完成对instance的实例化,随后的线程发现 null == instance 不成立则无须进行任何操作,以后对getInstance的访问就不需要数据同步的保护了。
这种方式看起来那么的完美,既满足了懒加载,有保证instance实例的唯一性。Double-Check的方式提供了高效的数据同步策略,可以允许多个线程同时对getInstance进行访问。但是这种方式有可能引起空指针异常,我们分析一下。
Singleton4的构造函数中,初始化了msg还有Singleton2自身,根据JVM指令重排序和Happens-Before规则,这两者之间的实例化顺序并无前后关系的约束,那么极有可能instance最先被实例化,而msg并未完成实例化,未完成初始化的实例调用其他方法将会抛出空指针异常。
五、Volatile + Double + Check
private volatile static Singleton2 instance = null;
private String msg;
/**
* 私有化构造方法
*/
private Singleton2() {
msg = "初始化参数";
}
public static Singleton2 getInstance3() {
if (instance == null) {
synchronized (Singleton2.class){
if(instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
在instance前 加上 volatile的关键字,则可以防止重排序的发生。但终归加了synchronized
,对性能依旧造成了影响。有没有更好的方式呢?有!
六、Holder方式
public class Singleton3 {
private static Singleton3 instance = null;
private static class Holder{
private static final Singleton3 singleton3 = new Singleton3();
}
/**
* 私有化构造方法
*/
private Singleton3() {
}
public static final Singleton3 getInstance() {
return Holder.singleton3;
}
}
在Singleton6中并没有instance的静态变量,而是将其放在静态内部类Holder类中,因此Singleton3初始化过程中并不会创建Singleton3的实例,Holder类中定义了Singleton3的静态变量,并且直接进行了实例化,当Holder被直接引用的时候则会创建Singleton3的实例,该方法又是同步方法,保证了内存的可见性,JVM的顺序性和原子性。Holder方式是单例设计最好的设计之一。但是!依然优缺点。
反射破坏单例
public static void main(String[] args) throws Exception {
// 无聊情况下进行破坏
Class<?> clazz = Singleton3.class;
// 获取私有化构造方法
Constructor constructor = clazz.getDeclaredConstructor(null);
// 强制访问
constructor.setAccessible(true);
// 暴力初始化两次
Object o1 = constructor.newInstance();
Object o2 = constructor.newInstance();
System.out.println(o1);
System.out.println(o2);
System.out.println(o1 == o2);
}
运行结果
显然我们创建出来了两个实例。破坏了我们的初衷。我们来优化一次,在构造方法做限制,一旦重复创建,我们就抛异常。
public class Singleton3 {
private static class Holder{
private static final Singleton3 singleton3 = new Singleton3();
}
/**
* 私有化构造方法
*/
private Singleton3() {
if(Holder.singleton3 != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static final Singleton3 getInstance() {
return Holder.singleton3;
}
}
感觉上该单例已经完美了,然而还有可能被破坏。
序列化破坏单例
实现序列化
public class Singleton4 implements Serializable {
private static class Holder{
private static final Singleton4 singleton3 = new Singleton4();
}
/**
* 私有化构造方法
*/
private Singleton4() {
if(Holder.singleton3 != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static final Singleton4 getInstance() {
return Holder.singleton3;
}
}
测试代码
Singleton4 s1 = null;
Singleton4 s2 = Singleton4.getInstance();
FileOutputStream fos = new FileOutputStream("Singleton4.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Singleton4.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton4) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
运行结果
显然,单例又遭到破坏。如何解决呢?只需要添加readResolve
方法即可。
public class Singleton4 implements Serializable {
private static class Holder{
private static final Singleton4 singleton3 = new Singleton4();
}
/**
* 私有化构造方法
*/
private Singleton4() {
if(Holder.singleton3 != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static final Singleton4 getInstance() {
return Holder.singleton3;
}
private Object readResolve(){
return Holder.singleton3;
}
}
再看运行效果
有兴趣的同学可以查看JDK的源码,发现实际上,这里我们还是实例化了两次,只不过第二次创建的对象没有被返回而已。这样也会造成内存的不必要浪费。
七、注册式单例
注册时单例就是将每个实例登记到某个地方,使用唯一标识来获取实例。
枚举式单例
public enum Singleton5 {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static final Singleton5 getInstance() {
return INSTANCE;
}
}
惊喜的是,枚举类天生就防止反射破坏与序列化破坏,有兴趣的同学,可以查阅JDK源码。
容器式单例
枚举类虽然写法优雅,但是在类加载之时,就将所有对象初始化放在内存中,这其实与饿汉式
无异。容器式则是将Bean 放在 concurrentHashMap<String,Object>
中,详细可参照Spring IOC的实现,这里就不多做叙述了。