设计模式之单例模式 —— 一文彻底读懂单例模式

单例模式也是创建型模式的一种,在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例,而且自行实例化并且向整个系统提供这个实例。该类构造方法为私有,并 =提供一个全局访问点,比如公开一个getInstance()方法获取单个实例。单例模式在现实生活中应用也非常广泛。例如:ServletContext、 ServletContextConfig ;在 Spring 框架应用中的 ApplicationContext;数据库的连接池等。一般来说单例的创建有饿汉式,懒汉式、注册式(枚举方式)三种方式。

饿汉式:

“饿汉模式”就是我们所说的立即加载,饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。Spring 中 IOC容器 ApplicationContext 本身就是典型的饿汉式单例。如下为饿汉式创建单例的两种方式。

public class StarvingSingle {
    private static final StarvingSingle single = new StarvingSingle();
    private StarvingSingle() {	
    }
    public static StarvingSingle getInstance() {
	return single;
    }
}
public class StaticBlockSingle {
    private static final StaticBlockSingle staticBlockSingle;
    private StaticBlockSingle() {
    }
    static {
	staticBlockSingle = new StaticBlockSingle();
    }
    public static StaticBlockSingle getInstance() {
	return staticBlockSingle;
    }
}

上面使用饿汉式的两种方式创建了一个单例模式,没有加任何锁、执行效率比较高,用户体验比懒汉式单例模式更好,但是类加载的时候就初始化,不管用与不用都占着空间,在单例特别多的时候浪费了内存。而懒汉模式则解决了这个问题。

懒汉模式:

懒汉模式就是我们所说的延迟加载,懒汉式单例的特点是:被外部类调用的时候内部类才会加载,即在我们获取Instance实例的时候才会被实例化,如下为一个简单的单例案例:

public class UnsafeLazySingle {
    private static UnsafeLazySingle unsafeLazySingle;
    private UnsafeLazySingle() {
    }
    public static UnsafeLazySingle getInstance() {
	if (unsafeLazySingle == null) {
            unsafeLazySingle = new UnsafeLazySingle();
	}
	return unsafeLazySingle;
    }
}

上面的例子只有在获取实例的时候才被实例化,我们为该类起了一个UnsafeLazySingle,不安全的懒加载单例,因为再多个线程下,使用这种形式可能会产生多个实例。该方式存在线程安全隐患。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?我们可以给getInstance()加上synchronized关键字,使这个方法变成线程同步方法:

public class SynchronizedSingle {
    private static SynchronizedSingle single = null;
    private SynchronizedSingle() {
    }
    public synchronized static SynchronizedSingle getInstance() {
        if (single == null) {
            single = new SynchronizedSingle();
        }
        return single;
    }
}

使用上面的方式,当我们将其中一个线程执行并调用 getInstance()方法时,另一 个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻 塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance() 方法。使用这种方法线程安全的问题是解决了。但是,用 synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批 量线程出现阻塞,从而导致程序运行性能大幅下降。所以我们需要一种更好的方式,既兼顾线程安全又提升程序性能。这就是双重检查锁的单例模式:

public class SynchronizedDoubleCheckSingle {
    private static SynchronizedDoubleCheckSingle single = null;
    private SynchronizedDoubleCheckSingle() {
    }
    public static SynchronizedDoubleCheckSingle getInstance() {
        if (single == null) {
            synchronized (SynchronizedDoubleCheckSingle.class) {
                if (single == null) {
                    single = new SynchronizedDoubleCheckSingle();
                }
            }
        }
        return single;
    }
}

上面的两种懒加载模式,无论是不是使用双重锁检测都用到了synchronized关键字,总归是要上锁,对程序性能还是存在一定影响的。我们需要考虑一种不上锁的懒加载方式,从类初始化角度来考虑,可以采用静态内部类的方式:

public class InnerStaticSingle {
    private InnerStaticSingle() {
    }
    private static class InnerStatic {
        private static final InnerStaticSingle single = new InnerStaticSingle();
    }
    public static final InnerStaticSingle getInstance() {
        return InnerStatic.single;
    }
}

上面内部类方式创建单例兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题,又巧妙地避免了线程安全问题。上面我们使用多种方式创建了单例模式,并且解决了多线程下的安全问题,保证多线程时只有一个实例。但是其实这种创建方式还会有其它问题:

  1. 享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,会创建一个新的实例
  2. 读取序列化对象时,任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例。

 反射破坏单例:

上面的例子我们实现了单例模式,但是我们还可以使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会 两个不同的实例。这就破坏了单例模式,代码如下所示:

public class ReflectSingle {
    public static void main(String[] args) {
	try {
	    InnerStaticSingle innerStaticSingle = InnerStaticSingle.getInstance();
	    Constructor<InnerStaticSingle> constructor = InnerStaticSingle.class.getDeclaredConstructor();
	    constructor.setAccessible(true);
	    InnerStaticSingle innerStaticSingle1 = constructor.newInstance();
            //输出false        
            System.out.println(innerStaticSingle == innerStaticSingle1);
	} catch (Exception e) {
        }
    }
}

上面的代码最终输出的是false,很显然,创建了两个不同的实例。要解决这个问题,我们只需要在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。代码如下所示,如果要创建的实例已经存在,则构造方法中直接抛出异常。

private InnerStaticSingle() {
    if (InnerStatic.single != null) {
        throw new RuntimeException("不允许创建多个实例");
    }
}

序列化破坏单例:

除了通过反射破坏单例外,通过序列化也是可以破坏单例的。当我们将一个单例对象创建好,将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象时,反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当 于破坏了单例。测试代码如下:

public class SeriableStaticBlockSingle {
    public static void main(String[] args) {
	try {
		StaticBlockSingle sbs = StaticBlockSingle.getInstance();
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
		oos.writeObject(sbs);
		oos.flush();
		oos.close();
		FileInputStream fis = new FileInputStream("SerSingleton.obj");
		ObjectInputStream ois = new ObjectInputStream(fis);
		StaticBlockSingle sbs2 = (StaticBlockSingle) ois.readObject();
		//输入false
		System.out.println(sbs == sbs2);
		ois.close();
	} catch (Exception e) {

	}
    }
}

运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,那么,我们要保证序列化的情况下也能够实现单例,只需要增加 readResolve()方法即可。使用下面的代码,序列化之后反序列化的对象与获取的实例是一致的,上面的测试代码会输出true。至于原因可以查看JDK的ObjectInputStream类的readObject()方法的源码。

public class StaticBlockSingle implements Serializable {
    private static final long serialVersionUID = 6452924321256593732L;
    private static final StaticBlockSingle staticBlockSingle;
    private StaticBlockSingle() {
    }

    static {
	staticBlockSingle = new StaticBlockSingle();
    }

    public static StaticBlockSingle getInstance() {
	return staticBlockSingle;
    }
	
    public Object readResolve() {
	return staticBlockSingle;
    }
}

使用枚举创建单例:

用enum具有自由序列化,线程安全,保证单例的特性,也就是说,如果我们使用枚举创建单例模式,不用提供readResolve()方法即可保证序列化的对象与创建的对象一致。这也是Effect Java中推荐的创建单例模式的写法:如下就是最简单的枚举。

public enum EnumSingle {
    INSTANCE;
    private Object data;
    public Object getData() {
	return data;
    }
    public void setData(Object data) {
	this.data = data;
    }
    public static EnumSingle getInstance() {
        return INSTANCE;
    }

}

上面我们介绍了单例的几种实现方式与一些问题,单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。至于采用哪种方式创建单例,则需要根据项目的具体要求来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值